글 · 시리즈 내 사이트 만들기 · 2번째 글 / 총 3편
도메인 모양의 로고를 페이지 선택 메뉴로 만들기
2026.09.16
지난 글에서는 헤더의 시맨틱 마크업과 컴포넌트 분리를 고민했다. 이번에는 그 헤더에 어떤 내비게이션을 둘지 다시 생각했다.
처음에는 왼쪽에 minjun.me가 있고 별도 메뉴에 Portfolio 링크가 있었다. 헤더를 깔끔하게 만들고 싶어서 로고와 페이지 이동 메뉴를 합칠 방법을 고민했다. 이 글에서는 페이지의 역할을 나눈 뒤 로고에서 선택지를 펼치고 이동할 주소를 미리 보여주는 메뉴로 다듬은 과정을 정리한다.
헤더를 만든 뒤 바로 내비게이션을 바꾸지 않고 먼저 홈과 블로그에 무엇을 보여줄지 정리했다. About, 글 목록, 노트 목록을 차례로 만든 뒤 블로그 페이지를 구성했다.
홈과 블로그의 역할 나누기
원래 메인에는 자기소개인 About 아래로 글과 노트 목록이 이어져 있었다. 여기에서 메인은 소개를 보여주는 곳으로 두고 글과 노트는 블로그로 옮기고 싶었다.
About은 메인에만 둘 영역도 아니었다. 블로그와 포트폴리오에도 소개를 둘 생각이었다.
| 페이지 | 구성 |
|---|---|
| 홈 | About |
| 블로그 | About, 글 목록, 노트 목록 |
| 포트폴리오 | About을 두고 이후 콘텐츠 추가 |
먼저 공통으로 사용할 About 컴포넌트의 구조를 정했다. 페이지에서 받은 title은 제목으로, children은 소개 문단으로 렌더링한다. 아래 코드는 구조만 간단히 정리한 예시라서 긴 스타일 클래스와 GitHub, 이메일 링크의 세부 속성은 생략했다.
import type { ReactNode } from 'react';
import { cn } from '@/shared/lib';
interface AboutProps {
title: string;
children: ReactNode;
className?: string;
}
export function About({ title, children, className }: AboutProps) {
return (
<section id="about" className={cn('pt-24 sm:pt-32', className)}>
<h1>{title}</h1>
<p>{children}</p>
<div>{/* GitHub와 이메일 링크 */}</div>
</section>
);
}
About의 디자인은 공유하면서 제목과 본문은 페이지마다 전달하도록 했다. 지금은 세 페이지 모두 홈의 소개를 그대로 사용한다. 페이지별 소개 문구는 나중에 쓰기로 했다.
각 페이지에서는 공통 About에 제목과 본문을 전달한다. 아래는 블로그 페이지의 소개 부분만 가져온 코드다. 이 페이지의 소개를 나중에 바꾸더라도 공통 컴포넌트의 스타일은 유지된다.
<About title="김민준">
프론트엔드 개발을 공부하고 있습니다. <br />
복잡한 것을 이해하기 쉽게 만드는 과정과 <br />
개발하면서 발견한 것들을 기록합니다. <br />
</About>
글과 노트 상세 페이지의 ‘목록으로 돌아가기’ 링크도 홈 대신 /blog를 가리키도록 바꿨다. 목록을 옮겼으니 돌아오는 경로도 함께 바뀌어야 했다.
이렇게 페이지의 역할을 정한 뒤, 사이트 이름과 페이지 이동 메뉴를 하나로 합치는 방향으로 헤더를 다시 설계했다.
로고 안에 메뉴를 넣어 보기
첫 아이디어는 minjun.me의 me를 드롭다운으로 만드는 것이었다. 여기에서 portfolio를 고르면 포트폴리오 페이지로 이동하고 로고도 minjun.portfolio로 바뀌도록 했다. 따로 있던 Portfolio 메뉴는 없앴다.
그다음에는 평소에 minjun.me만 보이다가 마우스를 올리면 가운데가 벌어지는 모습을 떠올렸다. minjun.과 .me 사이에 blog나 portfolio가 들어가는 방식이었다.
홈 minjun.me
블로그 minjun.blog.me
포트폴리오 minjun.portfolio.me
메뉴 상자나 배경을 크게 추가하고 싶지는 않았다. 기존 사이트의 글자 크기와 색감은 유지하면서 로고에서 선택지가 자연스럽게 드러나는 정도로만 표현하고 싶었다.
가운데가 벌어지는 모습은 구현 전에 떠올린 아이디어였다. 실제 메뉴는 SiteSwitcher 컴포넌트로 분리했다. 현재 페이지를 기준으로 선택지를 정렬하고 로고의 일부를 선택지로 쓰는 구조를 먼저 만들었다.
도메인의 순서를 잘못 생각했다
로고를 만들다 보니 실제 주소도 같은 형태로 쓰고 싶어졌다. minjun.me를 도메인으로 두고 블로그는 minjun.blog.me, 포트폴리오는 minjun.portfolio.me로 나누면 어떨까 했다.
이때 도메인의 순서를 잘못 생각했다는 걸 알았다. minjun.me 아래에 서브도메인을 두려면 blog.minjun.me와 portfolio.minjun.me가 되어야 한다. minjun.blog.me는 blog.me 아래의 주소다.
그래서 로고의 순서도 바꿨다.
홈 minjun.me
블로그 blog.minjun.me
포트폴리오 portfolio.minjun.me
선택하는 부분이 왼쪽으로 옮겨졌으니 화살표도 왼쪽에 두었다. 오른쪽 끝에 있을 때보다 선택 대상인 blog, portfolio와 가까워졌다.
이때 화면에 보여주는 이름과 실제로 이동하는 주소는 따로 다뤄야 한다는 것도 알게 됐다. 우선은 화면에서 /, /blog, /portfolio를 오가도록 만들고 실제 도메인 연결은 나중에 해결하기로 했다.
로고에서 페이지 선택이 가능하다는 걸 어떻게 알게 할까
SiteSwitcher를 실제 화면에서 보고 나니 다른 문제가 보였다. 처음 들어온 사람은 minjun.me에서 페이지를 바꿀 수 있다는 사실을 모를 수 있었다. 메뉴가 있다는 걸 알려면 마우스를 올려야 했지만 처음 방문한 사람이 그 위에 마우스를 올릴 이유는 없었다.
그래서 작은 아래쪽 화살표를 항상 표시했다. 메뉴가 열리면 화살표가 위로 돌아간다. 설명을 덧붙이지 않아도 펼칠 수 있는 요소라는 걸 알리고 싶었다.

메뉴를 닫아도 현재 페이지 이름은 계속 보이도록 했다. 블로그에 있다면 blog.minjun.me가 보여야 지금 어디에 있는지 바로 알 수 있기 때문이다. 메뉴를 열어 선택지에 마우스를 올리면 로고의 페이지 이름도 그 선택지에 맞춰 실시간으로 바뀐다.
처음에는 가운데가 벌어지는 효과를 구상했지만 내비게이션에서는 현재 어디에 있는지와 어디를 누를 수 있는지가 먼저 보여야 한다고 생각했다.
선택하기 전에 주소를 미리 보여주기
SiteSwitcher를 구현하면서 선택하기 전에 결과를 보여주는 동작도 함께 넣었다. portfolio에 마우스를 올리면 로고가 portfolio.minjun.me로 바뀌고 공백 선택지에 올리면 minjun.me가 된다.
실제 페이지로 이동하는 건 클릭했을 때뿐이다. 마우스가 선택지에서 벗어나면 로고는 현재 페이지 이름으로 돌아온다.
구현에서는 현재 위치와 미리보기 상태를 따로 다뤘다. 현재 위치는 어느 페이지에 있는지를 나타내고 미리보기는 지금 가리키는 선택지를 잠시 저장한다. 미리보기 대상이 있으면 그 이름을, 없으면 현재 페이지 이름을 표시한다.
미리보기를 켜도 메뉴 순서는 바뀌지 않도록 했다. 선택지가 다른 자리로 움직이면 클릭하기 어려워진다. 메뉴 순서는 현재 페이지를 기준으로 두고 로고에 표시하는 접두어만 바꾼다.
메뉴 선택지는 sections 배열에 담았다. 각 항목의 label은 로고 앞에 붙일 접두어이고 href는 이동할 내부 경로다. 홈은 접두어가 없으므로 label을 빈 문자열로 둔다.
const sections = [
{ label: '', href: '/' },
{ label: 'blog', href: '/blog' },
{ label: 'portfolio', href: '/portfolio' },
];
이 배열은 currentHref를 기준으로 정렬해 현재 페이지를 첫 번째에 둔다. 로고에 표시할 항목은 previewHref로 고른다. 아래는 컴포넌트 안의 상태와 선택 로직만 간단히 정리한 예시다. currentHref는 현재 사이트를 나타내는 /, /blog, /portfolio 중 하나이며 경로와 도메인에서 이 값을 구하는 부분은 생략했다.
const [open, setOpen] = useState(false);
const [previewHref, setPreviewHref] = useState<string | null>(null);
// 현재 페이지를 맨 앞에 둔다. hover 상태는 정렬에 관여하지 않는다.
sections.sort(
(a, b) => Number(b.href === currentHref) - Number(a.href === currentHref)
);
const preview =
sections.find(({ href }) => href === previewHref) ?? sections[0];
function closeMenu() {
setOpen(false);
setPreviewHref(null);
}
sort의 비교 함수는 두 항목 a, b 중 어떤 항목을 앞에 둘지 숫자로 판단한다. href === currentHref의 결과는 true 또는 false인데 Number로 감싸면 각각 1과 0이 된다. 현재 항목이 a라면 0 - 1이 되어 a가 앞에 오고, 현재 항목이 b라면 1 - 0이 되어 b가 앞에 온다. 두 항목 모두 현재 항목이 아니면 결과가 0이라 순서는 그대로 둔다.
previewHref가 null이면 첫 항목인 현재 페이지를 표시한다. 다른 선택지를 가리키면 그 주소를 저장하고 선택지에서 벗어나거나 메뉴를 닫으면 다시 null로 돌린다. sections.map으로 만드는 링크에는 다음 이벤트를 연결했다. 첫 항목은 로고에 표시하는 자리라서 미리보기 대상을 설정하지 않는다.
onPointerEnter={() => {
if (index !== 0) setPreviewHref(href);
}}
onPointerLeave={() => setPreviewHref(null)}
onFocus={() => {
if (index !== 0) setPreviewHref(href);
}}
onBlur={() => setPreviewHref(null)}
onClick={closeMenu}
여기서 호버와 포커스는 미리보기 상태만 바꾼다. 실제 이동은 링크의 href가 담당하고 클릭 핸들러는 메뉴와 미리보기를 닫는다.
마우스뿐 아니라 키보드로 선택지에 포커스했을 때도 미리보기를 적용했다. ESC를 누르면 메뉴를 닫고 미리보기를 초기화한다. 메뉴가 닫혀 있을 때는 보이지 않는 선택지로 키보드 포커스가 이동하지 않는다.
메뉴를 여는 버튼은 useRef로 기억한다. 메뉴를 감싼 요소에서 Escape 키를 감지하면 그 버튼으로 포커스를 돌려주고 메뉴를 닫는다. 펼쳐지지 않은 선택지에는 inert를 지정해 키보드 포커스와 상호작용 대상에서 제외한다.
const trigger = useRef<HTMLButtonElement>(null);
<div
onKeyDown={(event) => {
if (event.key === 'Escape') {
trigger.current?.focus();
closeMenu();
}
}}
>
<button ref={trigger} onClick={() => setOpen(true)}>
섹션 선택
</button>
{sections.map(({ href }, index) => (
<Link
key={href}
href={index === 0 ? preview.href : href}
inert={index !== 0 && !open}
onFocus={() => {
if (index !== 0) setPreviewHref(href);
}}
>
선택지
</Link>
))}
</div>
고정된 부분은 그대로 두기
SiteSwitcher에서는 minjun.me를 고정하고 앞부분만 선택지로 표시했다. minjun.me는 어느 페이지에서도 같기 때문에 전체 주소를 반복해서 보여주기보다 바뀌는 부분만 선택하는 편이 내가 원했던 모습에 가까웠다.
블로그에서는 blog.가 붙고 포트폴리오에서는 portfolio.가 붙는다. 홈에서는 접두어와 점이 모두 사라진다.
홈 선택지를 어떻게 보여줄지도 문제였다. 홈에는 접두어가 없으니 선택지도 비어 있어야 했다. 하지만 아무것도 보이지 않으면 선택할 자리인지 알기 어려웠다.
홈 선택지는 몇 번 바꿔 봤다. 처음에는 ‘홈’이라는 글자를 넣었다가 공백으로 바꿨다. 공백을 드러내려고 옅은 배경과 점선 테두리를 넣어 보기도 했지만 글자 위주의 메뉴에서 박스만 눈에 띄었다. 마지막에는 ____로 표시했다.

이 밑줄은 주소에 들어가는 문자가 아니다. 접두어가 없는 홈을 선택한다는 표시다. 선택하면 로고는 ____.minjun.me가 아니라 minjun.me가 된다. 화면 읽기 도구에는 여전히 ‘홈’이라는 이름을 전달하도록 했다.
데이터에는 홈의 이름을 빈 문자열로 두었다. ____는 메뉴를 보여줄 때만 사용한다. 아래는 선택지 데이터와 홈 선택지의 표시 방식을 간추린 예시다. 실제 코드에서는 현재 페이지를 먼저 배치한다.
const sections = [
{ label: '', href: '/' },
{ label: 'blog', href: '/blog' },
{ label: 'portfolio', href: '/portfolio' },
];
// 펼쳐진 메뉴의 홈 선택지
<Link href="/" aria-label="홈">
<span aria-hidden="true">____</span>
</Link>;
로고의 접두어는 이렇게 표시한다. preview는 현재 표시할 항목이다. 앞 절에서 설명한 미리보기 상태에 따라 바뀐다. 홈의 label은 빈 문자열이므로 점도 함께 생략된다.
{
preview.label ? `${preview.label}.` : null;
}
이 시점의 링크는 아직 /, /blog, /portfolio 같은 내부 경로를 사용하고 있었다. 화면에 보이는 도메인과 실제 이동 주소를 연결하는 일은 다음에 따로 해결했다.
헤더 링크를 실제 도메인에 연결하기
이후에는 화면에 표시한 이름과 실제 이동 주소를 연결했다. 현재 호스트 이름을 확인하고 커스텀 도메인을 사용 중일 때 선택지의 경로를 실제 도메인으로 바꾸도록 했다.
const siteUrls: Record<string, string> = {
'/': 'https://minjun.me',
'/blog': 'https://blog.minjun.me',
'/portfolio': 'https://portfolio.minjun.me',
};
const sectionHref = (path: string) => (isCustomDomain ? siteUrls[path] : path);
로컬에서는 /blog 같은 경로를 쓰고 커스텀 도메인에서는 blog.minjun.me로 이동하도록 했다. 하지만 링크 주소만 바꿔서는 서브도메인에서 블로그 페이지를 보여줄 수 없었다.
여기까지는 SiteSwitcher가 어떤 주소로 이동할지를 정한 부분이다. 실제로 minjun.me/blog를 서브도메인으로 바꾸고 blog.minjun.me/를 내부의 /blog 페이지에 연결하는 일은 Next.js 설정에서 처리해야 한다.
실제 도메인에서 메뉴 사용해 보기
도메인 연결까지 마친 뒤에는 메뉴를 실제로 사용해 보며 확인했다.
minjun.me에서 메뉴를 열고blog를 선택한다.- 주소가
blog.minjun.me로 바뀌고 블로그 페이지가 보이는지 확인한다. - 블로그에서 메뉴를 열고
portfolio를 선택한다. - 주소가
portfolio.minjun.me로 바뀌고 포트폴리오 페이지가 보이는지 확인한다. - 포트폴리오에서
minjun.me를 선택해 메인으로 돌아온다.

처음에는 현재 경로만 비교해서 헤더가 현재 사이트를 잘못 표시하는 문제도 있었다. 예를 들어 https://blog.minjun.me/에 접속하면 주소창의 경로는 /지만, Next.js는 rewrite를 통해 내부의 /blog 페이지를 렌더링한다. 화면에 보이는 주소를 바꾸지 않은 채 다른 경로의 페이지를 보여주기 때문에, pathname만 확인하면 블로그를 홈으로 잘못 판단할 수 있다. 실제 hostname도 함께 확인하도록 고친 뒤에는 블로그에서 blog.minjun.me, 포트폴리오에서 portfolio.minjun.me가 현재 위치로 표시됐다.
마무리
이번 작업은 Portfolio 링크 하나를 로고에 합쳐 보자는 생각에서 시작했다. 먼저 홈과 블로그의 역할을 나누고 그다음 SiteSwitcher를 만들었다. 실제 화면을 확인하면서 화살표와 미리보기를 추가했고 홈 선택지는 ____로 보여줬다. 마지막으로 화면의 선택지와 실제 도메인 사이의 이동을 연결했다.
처음 구상한 움직임을 화면에서 확인하며 하나씩 다듬었다. 처음 생각을 그대로 지키기보다, 직접 봤을 때 어색한 부분을 고치는 게 더 중요했다. 지금 헤더에는 작은 화살표와 접두어 선택지가 남아 있다. 이 두 요소가 현재 위치 표시와 페이지 이동을 함께 맡는다. Vercel과 DNS 설정처럼 도메인 자체를 연결하는 과정은 다음 글에서 이어서 다룬다.
페이지별 About 내용을 채우고 포트폴리오 콘텐츠를 추가하려 한다.