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

구글 서치콘솔에 "색인 생성 안 됨"이 뜨는 진짜 이유

크롤링됨·감지됨·제외됨의 차이부터 실제로 손볼 포인트까지 — 서치콘솔 색인 오류의 숨은 원인과 회복 순서를 실무 관점에서 정리했습니다.

6 min read
기술 SEO

관리자 table은 OK, 가격표는 div grid — table 코드 리뷰

데이터는 table이고 레이아웃은 grid입니다. 이 한 줄을 거꾸로 적용한 페이지는 AI가 가격을 못 읽고, 스크린리더가 행과 열을 못 잡습니다. 두 케이스의 경계를 코드로 그어봅니다.

9 min read
기술 SEO

IndexNow vs Google Indexing API: 실시간 색인 요청 비교 가이드

IndexNow와 Google Indexing API의 지원 엔진, 콘텐츠 제한, 쿼터를 10가지 항목으로 비교합니다. 네이버 IndexNow 설정 방법과 Next.js 구현 가이드를 포함합니다.

6 min read
기술 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

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

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