SOYOYU
블로그로 돌아가기기술 SEO

Next.js layout.tsx에 main이 두 개 — landmark 코드 리뷰

main·nav·header·footer 같은 landmark 태그는 페이지의 지도입니다. 지도가 두 장이거나 없으면 스크린리더 사용자도 AI 에이전트도 길을 잃습니다. 가장 자주 잘못된 케이스부터 정리합니다.

전승엽2026년 7월 11일9 min read
A11y 코드 리뷰시맨틱 HTMLaccessibility treeAI 에이전트웹 접근성landmarkmainnavNext.jsKWCAG

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"

다른 글 읽기

기술 SEO

프로그래매틱 SEO: 대규모 페이지 자동 생성과 품질 통제

Google의 Scaled Content Abuse 정책 강화 속에서 프로그래매틱 SEO를 안전하게 실행하는 방법을 다룹니다. Zapier, Canva, Wise의 실제 사례와 30-40% 고유 콘텐츠 기준을 정리합니다.

7 min read
기술 SEO

네이버 Search Advisor 기술 감사 워크플로우: GSC와의 차이와 6단계 실전 가이드

네이버 서치어드바이저의 6개 핵심 기능을 분석하고, Google Search Console과 13개 항목을 비교합니다. 네이버에 최적화된 6단계 기술 감사 워크플로우를 제공합니다.

7 min read
기술 SEO

h1은 한 개만이라는 미신과 스크린리더 사용자의 실제 길찾기 — heading 코드 리뷰

SEO 도구는 'h1은 한 개여야 한다'고 외치지만 스크린리더 사용자는 헤딩 목록으로 페이지를 탐색합니다. 헤딩의 본질은 SEO가 아닌 목차이며, 그래서 AI Overviews 인용 신호이기도 합니다.

11 min read
기술 SEO

보안 헤더(CSP/HSTS)가 검색 순위에 미치는 영향

HTTPS는 약 12%의 순위 요인으로 확인되었지만, John Mueller는 보안 헤더가 직접적 순위 요인이 아니라고 밝혔습니다. '상위 사이트 71%가 보안 헤더 사용'이라는 통계의 검증 결과, 실제 채택률 데이터, 상관관계와 인과관계의 구분을 정리합니다.

8 min read
기술 SEO

Edge SEO 실전: 리다이렉트, 헤더 인젝션, HTML 변환 코드 예제

Cloudflare Workers로 구현하는 Edge SEO 실전 코드. 301 리다이렉트, KV 대량 리다이렉트, HSTS/CSP/X-Robots-Tag 헤더 인젝션, HTMLRewriter 메타 태그 삽입, 캐노니컬 인젝션, hreflang 인젝션, A/B 테스트까지.

8 min read
기술 SEO

Edge SEO: Cloudflare Workers로 SEO 변경사항 즉시 배포

Edge SEO의 정의(Dan Taylor 제안), 8가지 활용 사례, TTFB 60~80% 감소, 1ms 미만 실행 시간, 무료 플랜 10만 요청/일. 개발자 대기 없이 기술 SEO를 즉시 배포하는 방법을 정리합니다.

7 min read

검색 최적화가 필요하신가요?

무료 상담을 통해 비즈니스에 맞는 최적화 전략을 확인하세요.