A11y 코드 리뷰 #006 · 오늘 도감에 등록할 태그 —
<main>,<nav>,<header>,<footer>,<aside>(landmark 5인방)이번 도감 카드는 페이지의 지도를 만드는 태그들입니다. 잘못 쓰면 사용자도 AI도 페이지에서 길을 잃습니다. 가장 자주 깨지는 케이스 — Next.js의 layout.tsx와 page.tsx에 main이 두 개 들어가는 패턴부터 봅니다.
TL;DR
- landmark 태그는 페이지의 지도입니다. 스크린리더 사용자는 D 키 또는 rotor의 "랜드마크" 항목으로 페이지의 구역을 한눈에 봅니다.
<main>은 페이지당 정확히 한 개여야 합니다. layout.tsx에 main을 두고 page.tsx에 또 두면 둘이 됩니다.<header>와<footer>는 페이지 전체용으로도, article 내부용으로도 쓰입니다. 컨텍스트가 다르면 의미가 자동으로 다르게 노출됩니다.<nav>가 페이지에 여러 개일 때는aria-label로 구분해야 합니다 — "주 내비게이션", "푸터 내비게이션".- AI 에이전트와 검색엔진 모두 페이지의 "본문 영역"을 식별할 때
<main>을 가장 강한 신호로 봅니다. div로 만든 컨테이너는 그 신호가 없습니다.
1. 어디서나 본 그 landmark 누락
다음 네 패턴은 한국 사이트뿐 아니라 글로벌 사이트에서 매일 보는 안티패턴입니다.
Next.js layout.tsx의 main 중복
// app/layout.tsx
export default function RootLayout({ children }: Props) {
return (
<html>
<body>
<header>...</header>
<main>{children}</main>
<footer>...</footer>
</body>
</html>
);
}
// app/blog/[slug]/page.tsx
export default function BlogPost() {
return (
<main> {/* ❌ 두 번째 main */}
<article>...</article>
</main>
);
}
모든 게 div
<div className="header">
<div className="logo">...</div>
<div className="nav">...</div>
</div>
<div className="main-content">
<div className="article">...</div>
</div>
<div className="footer">...</div>
nav 여러 개 라벨 없음
<nav>
<a href="/">홈</a>
<a href="/about">소개</a>
</nav>
...
<nav> {/* 두 번째 nav, 어떤 nav인지 모름 */}
<a href="/privacy">개인정보처리방침</a>
<a href="/terms">이용약관</a>
</nav>
aside의 오용
<aside>
<h2>관련 글</h2> {/* OK */}
...
</aside>
<aside>
<h2>본문</h2> {/* ❌ aside는 본문이 아님 */}
...
</aside>
네 패턴 모두 마우스 사용자에게는 시각적으로 같은 페이지로 보입니다. 차이는 스크린리더의 landmark 목록, AI 에이전트의 본문 영역 식별, 검색엔진의 본문 가중치 평가에서 나타납니다.
2. 한 가지 질문
페이지를 처음 들어온 사용자가 "오늘 내가 보러 온 본문은 어디지?"라고 물었을 때, 보조 기술과 AI는 단 한 번에 그 위치를 찾을 수 있을까요?
답이 예면 landmark가 잘 정리된 페이지입니다. 아니오면 처음부터 끝까지 듣거나 OCR로 뒤져야 합니다.
3. landmark 5인방 — 점진적 해부
3.1 가장 단순한 페이지 구조
<body>
<header>
<a href="/" aria-label="홈으로"><img src="/logo.svg" alt="" /></a>
<nav aria-label="주 내비게이션">...</nav>
</header>
<main>
<article>
<h1>오늘의 글</h1>
...
</article>
</main>
<footer>
<nav aria-label="푸터 내비게이션">...</nav>
<p>© 2026 소요유</p>
</footer>
</body>
이 다섯 줄이 만드는 accessibility tree:
banner (= header)
└─ navigation [name="주 내비게이션"]
main
└─ article
└─ heading [level=1] "오늘의 글"
contentinfo (= footer)
└─ navigation [name="푸터 내비게이션"]
스크린리더 사용자가 D 키를 누르면 위 5개 landmark가 목록으로 뜹니다. 원하는 곳으로 한 번에 점프 가능합니다.
3.2 main — 페이지의 본문
<main>은 페이지의 주요 콘텐츠입니다. 페이지마다 한 개만 있어야 하고, 다른 main 안에 중첩될 수 없습니다.
<!-- ❌ 두 개의 main -->
<main>
<h1>왼쪽 패널</h1>
</main>
<main>
<h1>오른쪽 패널</h1>
</main>
<!-- ✅ 한 개의 main 안에 섹션들 -->
<main>
<section aria-labelledby="left-heading">
<h2 id="left-heading">왼쪽 패널</h2>
</section>
<section aria-labelledby="right-heading">
<h2 id="right-heading">오른쪽 패널</h2>
</section>
</main>
main 안에는 header·nav·footer가 들어가도 됩니다. 그 경우 의미가 article 내부의 헤더/푸터로 바뀝니다.
3.3 header와 footer — 컨텍스트 의존 의미
<!-- 페이지 전체의 header -->
<body>
<header> ← banner role
<nav>...</nav>
</header>
</body>
<!-- article 내부의 header -->
<article>
<header> ← banner가 아닌 일반 헤더
<h1>글 제목</h1>
<p>작성자 · 날짜</p>
</header>
<p>본문...</p>
<footer> ← 일반 푸터
<p>관련 태그</p>
</footer>
</article>
<body> 직속의 <header>는 banner landmark role을 자동으로 받습니다. <article> 안의 <header>는 단순 그룹 컨테이너입니다. 같은 태그가 위치에 따라 의미가 자동으로 달라집니다.
3.4 nav — 여러 개면 라벨이 필수
페이지에 nav가 한 개면 라벨 없이도 OK입니다 ("navigation"으로 인식). 여러 개면 각각을 구분하는 라벨이 있어야 합니다.
<header>
<nav aria-label="주 내비게이션">
<a href="/">홈</a>
<a href="/services">서비스</a>
</nav>
</header>
<aside>
<nav aria-label="목차">
<a href="#section-1">섹션 1</a>
<a href="#section-2">섹션 2</a>
</nav>
</aside>
<footer>
<nav aria-label="푸터 링크">
<a href="/privacy">개인정보처리방침</a>
<a href="/terms">이용약관</a>
</nav>
</footer>
스크린리더 목록에 "navigation, 주 내비게이션", "navigation, 목차", "navigation, 푸터 링크"로 구분돼 나옵니다.
3.5 aside — 본문에서 분리 가능한 보조 콘텐츠
<aside>는 없어도 본문이 성립하는 보조 콘텐츠입니다.
<main>
<article>
<h1>본문</h1>
<p>...</p>
</article>
<aside aria-labelledby="related">
<h2 id="related">관련 글</h2>
...
</aside>
</main>
본문과 같이 보이는 사이드바, 광고, 부가 정보가 aside입니다. 본문의 일부면 aside가 아닌 section입니다.
4. 왜 문제인가 — 3중 영향
4a. 스크린리더 사용자
WebAIM Screen Reader User Survey의 일관된 결과 하나 — 시각장애 사용자가 페이지 탐색에서 헤딩 다음으로 자주 쓰는 단축키가 landmark 단축키입니다.
NVDA: D 키 → 다음 landmark
JAWS: R 키 → 다음 region/landmark
VoiceOver: rotor → "Landmarks"
landmark가 정리된 페이지에서는 사용자가 "본문으로 이동"을 한 번에 합니다. div로만 된 페이지에서는 — 모든 페이지가 똑같이 들립니다. 페이지의 구조 자체가 사라집니다.
4b. 키보드 사용자
키보드 사용자에게 landmark의 직접 이득은 skip link입니다.
<a href="#main" className="sr-only-focusable">본문으로 건너뛰기</a>
...
<main id="main">...</main>
skip link가 의미가 있으려면 — 건너뛸 main이 명시적이어야 합니다. div로만 된 페이지에서는 skip link가 임의의 div로 점프할 뿐, 의미적 의미가 약합니다.
4c. AI 에이전트와 검색엔진
검색엔진의 본문 가중치 평가 — 구글은 페이지의 주요 콘텐츠 영역을 식별합니다. 사이드바·헤더·푸터에 있는 텍스트는 보조 신호로, main 안의 텍스트는 주요 신호로 가중치를 다르게 둡니다. 시맨틱 main이 있는 페이지는 이 판단이 쉽고 정확합니다. 모든 게 div인 페이지에서는 어디까지가 본문인지를 추측해야 합니다.
AI Overviews와 Perplexity의 본문 추출 — 답변을 추출할 때, AI는 main 안의 텍스트를 1순위로, aside·footer의 텍스트를 후순위로 봅니다. 잘 정리된 landmark는 우리 페이지에서 어디를 인용해야 하는지를 AI에게 직접 알려줍니다.
AI 에이전트의 페이지 탐색 — 에이전트가 "이 페이지의 본문에서 가격 정보를 찾아줘"라는 작업을 받았을 때 — main이 있으면 한 번에 찾습니다. 없으면 페이지 전체를 OCR/파싱해야 합니다.
선행 글 구글이 'AI 검색 최적화 가이드'를 냈는데, 정작 핵심은 접근성 트리였습니다에서 봤듯 — accessibility tree는 landmark를 명시적인 region으로 노출합니다. div는 안 노출됩니다.
5. SEO/GEO/AEO 영향
5.1 SEO — main의 본문 가중치 신호
구글의 페이지 평가에서 main 안의 콘텐츠는 주요 신호입니다. 사이드바·푸터의 부가 텍스트는 보조입니다. 이게 직접적인 ranking factor라기보다는 — 페이지의 주제 적합도를 판단할 때 main 안의 키워드가 더 무겁게 평가됩니다.
대표적 케이스 — 같은 키워드가 footer에만 있는 페이지보다 main 안의 h1 근처에 있는 페이지가 그 키워드에 대해 더 적합한 페이지로 평가됩니다.
5.2 GEO — AI 에이전트가 보는 "본문 영역"
GEO의 실용적 정의 — AI 에이전트가 우리 페이지를 작업의 대상으로 봤을 때 정확하게 다룰 수 있는가. main이 있는 페이지는 에이전트가 "본문 영역"을 한 번에 식별합니다. 없으면 시각적 비율(가장 큰 영역)이나 콘텐츠 밀도로 추측합니다. 추측은 자주 틀립니다.
5.3 AEO — landmark와 AI Overviews의 답변 추출
AI Overviews가 답변을 추출할 때 어디까지를 본문으로 볼지를 판단해야 합니다. main이 있으면 그 안만 봅니다. 없으면 페이지 전체를 봅니다. 결과적으로 main이 있는 페이지가 더 정확한 답변을 인용받습니다 — 광고나 사이드바 텍스트가 답변에 섞이지 않기 때문입니다.
6. React에서 (특히 Next.js)
6.1 Next.js App Router의 layout과 page
가장 자주 깨지는 패턴 — layout.tsx에도 main, page.tsx에도 main. 두 main이 동시에 렌더됩니다.
// ❌ 깨진 패턴
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
<header>...</header>
<main>{children}</main> {/* 1번 main */}
<footer>...</footer>
</body>
</html>
);
}
// app/blog/[slug]/page.tsx
export default function BlogPost() {
return (
<main className="container"> {/* 2번 main */}
<article>...</article>
</main>
);
}
// ✅ 올바른 패턴 — layout에 main, page에는 article
// app/layout.tsx
<main>{children}</main>
// app/blog/[slug]/page.tsx
<article>
<h1>제목</h1>
...
</article>
layout에 main이 있으면 page에는 article·section·div만 쓰면 됩니다.
6.2 컴포넌트 라이브러리에서
대부분의 React UI 라이브러리는 landmark를 직접 렌더하지 않습니다 — landmark는 페이지 단위 결정이기 때문입니다. 컴포넌트는 div·section·article 안에서 동작하고, 페이지 컴포저(layout.tsx)가 landmark를 결정합니다.
예외 — Headless UI나 Radix의 일부 컴포넌트는 의미적으로 nav 또는 dialog를 자동으로 렌더합니다. 시리즈 #3에서 본 Radix Dialog가 <div role="dialog">를 자동 렌더하는 것과 같은 맥락.
6.3 skip link 패턴
// app/layout.tsx
<body>
<a
href="#main-content"
className="absolute left-0 top-0 -translate-y-full focus:translate-y-0"
>
본문으로 건너뛰기
</a>
<header>...</header>
<main id="main-content" tabIndex={-1}>
{children}
</main>
</body>
tabIndex={-1}은 main이 프로그램적으로 포커스를 받을 수 있게 합니다. skip link 클릭 시 main에 포커스가 잡히고, 다음 Tab이 main 안의 첫 인터랙티브 요소로 갑니다.
7. 한국 맥락 — 가볍게
KWCAG 관련 항목:
- 2.4.1 반복 영역 건너뛰기: 페이지마다 반복되는 영역(헤더·내비)을 건너뛸 수 있어야 합니다. landmark + skip link 조합이 이 항목의 표준 구현입니다.
- 1.3.1 정보·기능·관계의 인식 가능성: 콘텐츠의 구조와 관계가 보조 기술에 노출돼야 합니다. landmark가 빠지면 페이지의 영역 관계가 사라집니다.
한국 사이트에서 자주 보는 패턴 — 공공기관 사이트의 표준화된 landmark. 정부24, 국세청 같은 공공 사이트는 KWCAG 의무 준수라 landmark가 정확히 들어가 있습니다. 민간 사이트들이 더 자주 누락합니다.
8. "div로 landmark role을 주면 안 되나요?"
자주 듣는 반론들:
<div role="main">은 main과 같은가요? — 트리에서는 같은 main으로 노출됩니다. 다만 — 시맨틱 태그가 있는데 굳이 div + role을 쓰는 건 WAI-ARIA의 첫 번째 규칙 위반입니다: "native HTML element를 사용 가능하면 ARIA로 그것을 흉내 내지 마라".
header와 main을 같은 컨테이너에 둘 수 있나요? — <body> 직속에 <header>, <main>, <footer>가 형제 관계로 있는 게 표준입니다. 하나의 wrapper div로 묶지 마세요 — 묶으면 landmark의 의미가 약해집니다.
section과 article은 landmark인가요? — 기본적으로는 아닙니다. section과 article은 accessible name이 있을 때만 landmark의 일종(region 또는 article role)으로 노출됩니다. <section aria-labelledby="x"> 또는 <article aria-label="x">처럼 라벨을 줘야 의미가 명확해집니다.
<aside>를 페이지에 여러 개 둬도 되나요? — OK이지만 각각 라벨이 필요합니다. <aside aria-label="관련 글">, <aside aria-label="광고"> 식으로.
9. 체크리스트
- 페이지에 정확히 한 개의
<main>이 있는가 (layout과 page에 중복되지 않았는가) -
<header>,<footer>가 body 직속으로 있고, banner/contentinfo 역할을 하는가 - 모든
<nav>에aria-label이 있는가 (여러 개 있을 때 특히) - 페이지의 모든 콘텐츠가 어떤 landmark 안에 들어가 있는가 (landmark 밖의 콘텐츠는 없는 콘텐츠로 보일 수 있음)
- skip link가 있고 main으로 점프하는가
-
<aside>가 본문에서 분리 가능한 보조 콘텐츠일 때만 쓰였는가 - HeadingsMap 같은 확장으로 봤을 때 landmark 트리가 깔끔한가
- AI Overviews 인용을 노리는 페이지에 main이 있는가
마무리
이 글이 16편 시리즈의 여섯 번째 도감 카드입니다. 다음 카드는 <table> — 관리자 화면 table은 OK인데 마케팅 페이지의 가격표는 div grid로인 그 흔한 패턴을 봅니다.
landmark의 단 한 줄 규칙 — 페이지는 5개 영역으로 나뉩니다 — header·nav·main·aside·footer. div로 만들지 마세요, 그게 곧 페이지의 지도입니다.
Next.js 앱의 landmark 진단 또는 SEO/GEO 친화 페이지 구조 컨설팅이 필요하시면 문의하기로 연락 주세요.
codex:
primary_tag: main
secondary_tags: [nav, header, footer, aside]
aria_roles: [main, navigation, banner, contentinfo, complementary]
aria_attributes: [aria-label, aria-labelledby]
related_patterns: [skip-link, page-structure, nextjs-layout, document-outline]
kwcag: ["2.4.1", "1.3.1"]
wcag: ["2.4.1", "1.3.1", "1.3.6"]
ai_agent_impact: high
seo_geo_impact: high
difficulty: beginner
test_envs:
- "NVDA 2024.x + Chrome 125 (D 키)"
- "VoiceOver macOS 14 + Safari 17 (rotor → Landmarks)"
references:
- "WHATWG HTML Living Standard, Sections"
- "WAI-ARIA Authoring Practices, Landmark Regions"
- "WebAIM Screen Reader User Survey #10 (2024)"
- "Next.js Docs, Layouts and Pages"