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

이미지 검색에 우리 제품 사진이 하나도 안 나오는 이유

구글 이미지·시각 검색(Google Lens)에 제품 사진이 안 뜨는 원인과 대응. Alt Text·파일명·이미지 사이트맵·주변 텍스트까지 이커머스 시각 검색 최적화 실무.

4 min read
기술 SEO

커스텀 dropdown은 거의 항상 잘못된 선택입니다 — select와 combobox 코드 리뷰

native <select>는 모바일에서 압도적으로 좋은 UX를 줍니다. 커스텀 dropdown으로 갈아탈 정당한 이유는 검색 가능, 멀티 선택, 가상화 셋 정도뿐입니다. ARIA combobox의 진짜 비용을 봅니다.

9 min read
기술 SEO

검색 결과에 내 사이트 설명이 이상하게 나오는 이유

메타 디스크립션을 써도 구글이 다르게 보여주는 이유와 대응 방법. 구글이 스니펫을 재작성하는 구조, 자주 하는 실수, 최소화할 수 있는 기술.

4 min read
기술 SEO

type=text는 거의 항상 잘못된 선택입니다 — input 타입별 코드 리뷰

type 속성 한 글자가 모바일 키보드를 바꾸고, AI 자동완성 정확도를 결정하고, 한국 전화번호 입력의 함정을 만듭니다. input 타입 13개의 실무 결정 트리를 정리합니다.

8 min read
기술 SEO

고성 송지호·봉수대 오토캠핑장 예약 사이트가 검색에 안 나오는 이유

송지호·봉수대 등 고성군 공공캠핑장 6곳 예약 방법 정리. 그리고 공식 예약 사이트가 구글 색인에 0건인 이유를 구글 오픈소스 파서까지 돌려 실측으로 진단합니다.

19 min read
기술 SEO

신규 페이지를 만들어도 구글이 며칠째 안 가져가는 이유

사이트맵을 제출해도 신규 페이지가 바로 색인되지는 않습니다. 발견·크롤·색인 단계를 구분하고 공식 도구로 원인을 점검하는 대응 순서.

5 min read

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

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