Skip to main content

이제 드디어 '작은 기록 상점'에 간판을 달 시간입니다.

앞 장에서는 어떤 앱을 만들지 설계했습니다. 홈 화면에는 무엇이 보일지, 기록 목록과 상세 화면은 어떤 흐름으로 이어질지 큰 그림도 잡아 두었죠. 이제 그 그림을 실제 화면으로 바꾸기 시작할 차례입니다.

이번 장의 주인공은 page.tsx와 layout.tsx입니다. 이름은 단순해 보이지만, 이 둘은 App Router를 이해하는 가장 중요한 출발점입니다. 특히 Next.js의 핵심 철학인 파일 기반 라우팅을 가장 쉽게 체감할 수 있는 지점이기도 합니다.

쉽게 말하면 이렇습니다.

폴더는 주소를 만들고, 파일은 화면을 만듭니다.

이 한 문장을 몸으로 이해하면, App Router가 갑자기 훨씬 덜 무섭게 느껴집니다.

App Router를 아주 쉽게 이해해 보기

우리는 보통 웹 페이지의 주소를 이렇게 생각합니다.

  • / 는 홈페이지

  • /records 는 기록 목록

  • /records/abc 는 어떤 기록의 상세 페이지

예전에는 이런 주소를 코드로 따로 등록하는 방식이 낯설지 않았습니다. 하지만 Next.js의 App Router는 조금 다르게 생각합니다.

"주소를 코드로 등록하지 말고, 폴더 구조로 표현하자."

그래서 app 폴더 안에 파일과 폴더를 만들면, 그것이 곧 URL 구조가 됩니다.

예를 들어 이런 구조라면,

app/
  page.tsx
  records/
    page.tsx

이렇게 연결됩니다.

  • app/page.tsx → /

  • app/records/page.tsx → /records

너무 단순해서 오히려 신기할 정도입니다. 라우터 설정 파일을 따로 찾아다닐 필요가 없습니다. 주소가 궁금하면 폴더를 보면 되고, 화면이 궁금하면 page.tsx를 보면 됩니다.

이것이 App Router의 첫 번째 매력입니다.

page.tsx는 무엇일까요?

page.tsx는 특정 주소에서 보여 줄 실제 화면입니다.

초보자 눈높이로 말하면, page.tsx는 "이 주소에 들어왔을 때 브라우저에 보일 본문"이라고 생각하면 됩니다.

예를 들어 app/page.tsx는 홈페이지 화면이고, app/about/page.tsx가 있다면 그것은 /about 화면입니다.

가장 단순한 page.tsx는 이렇게 생겼습니다.

export default function Page() {
  return <h1>안녕하세요</h1>;
}

이 정도만 있어도 Next.js는 이 파일을 하나의 페이지로 이해합니다.

중요한 점은, 폴더만 만든다고 페이지가 생기지는 않는다는 것입니다. 폴더 안에 page.tsx가 있어야 해당 경로가 실제로 열리는 페이지가 됩니다.

즉, 폴더는 길이고, page.tsx는 그 길 끝에서 만나는 화면입니다.

layout.tsx는 무엇일까요?

이번에는 layout.tsx를 봅시다.

layout.tsx는 여러 페이지에서 공통으로 사용하는 화면 틀입니다. 예를 들면 이런 것들이죠.

  • 사이트 제목

  • 상단 헤더

  • 공통 메뉴

  • 푸터

  • 전체 여백과 기본 구조

이걸 매 페이지마다 복붙해서 넣는다면 금방 귀찮아집니다. 그리고 한 군데 수정할 때마다 모든 파일을 다시 열어야 하죠. layout.tsx는 바로 이 문제를 해결합니다.

레이아웃은 children이라는 값을 받아서, 공통 틀 안에 현재 페이지를 끼워 넣습니다.

조금 귀엽게 비유하면, layout.tsx는 액자이고 page.tsx는 액자 안에 들어가는 그림입니다.

기본 모양은 보통 이렇게 생깁니다.

export default function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <header>공통 헤더</header>
      <main>{children}</main>
      <footer>공통 푸터</footer>
    </div>
  );
}

여기서 {children} 자리에 현재 페이지 내용이 들어옵니다.

즉, 홈페이지든 목록 페이지든 상세 페이지든, 공통으로 둘러싸는 바깥 틀은 layout.tsx가 담당합니다.

루트 레이아웃은 왜 꼭 필요할까요?

App Router에서는 app/layout.tsx가 특히 중요합니다. 이것을 루트 레이아웃이라고 부릅니다.

이 파일은 앱 전체의 최상단 틀이고, 반드시 있어야 합니다. 그리고 여기에는 html과 body 태그가 들어가야 합니다.

처음 프로젝트를 만들었을 때 자동으로 생성된 app/layout.tsx를 본 기억이 있을 것입니다. 그 파일이 바로 앱 전체의 뼈대입니다.

우리의 '작은 기록 상점'도 이 루트 레이아웃에서 시작합니다.

이번 장에서 만들 화면

이번 장에서는 복잡한 기능보다, 구조를 제대로 익히는 데 집중하겠습니다.

우리가 만들 것은 두 가지입니다.

  • 앱 전체에 공통으로 보일 기본 레이아웃

  • '작은 기록 상점'의 홈 화면

아직 링크 이동이나 동적 URL은 본격적으로 다루지 않습니다. 그건 다음 장에서 제대로 배울 예정입니다. 지금은 "폴더와 파일이 어떻게 화면이 되는지", 그리고 "레이아웃이 페이지를 어떻게 감싸는지"를 손에 익히는 것이 가장 중요합니다.

먼저 지금 구조를 확인해 봅시다

앞 챕터들에서 create-next-app으로 프로젝트를 만들었으니, 현재 app 폴더는 대략 이런 모습일 가능성이 큽니다.

app/
  favicon.ico
  globals.css
  layout.tsx
  page.tsx

이번 장에서는 이 기본 구조를 바탕으로 홈 화면을 우리 프로젝트에 맞게 바꿔 볼 것입니다.

루트 레이아웃을 '작은 기록 상점'답게 바꾸기

먼저 app/layout.tsx를 열어 봅시다.

기본으로 생성된 파일은 보통 아주 단순한 형태입니다. 이제 여기에 우리 앱의 공통 틀을 넣어 보겠습니다.

예를 들어 이렇게 작성할 수 있습니다.

import './globals.css';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <header>
          <div>
            <h1>작은 기록 상점</h1>
            <p>작지만 오래 남는 기록들을 진열하는 공간</p>
          </div>

          <nav>
            <span>홈</span>
            <span>기록 보기</span>
            <span>새 기록 쓰기</span>
          </nav>
        </header>

        <main>{children}</main>

        <footer>
          <p>© 2026 작은 기록 상점</p>
        </footer>
      </body>
    </html>
  );
}

이 코드는 아직 화려하지 않습니다. 하지만 구조를 이해하기에는 아주 좋습니다.

여기서 꼭 눈여겨볼 부분은 세 군데입니다.

첫째, <html lang="ko">입니다. 페이지 언어가 한국어라는 뜻입니다. 브라우저와 검색엔진이 페이지를 이해하는 데 도움이 됩니다.

둘째, <body> 안에 공통 헤더와 푸터가 들어 있습니다. 이제 앱의 어떤 페이지로 가더라도 이 바깥 구조는 공통으로 유지됩니다.

셋째, <main>{children}</main>입니다. 바로 이 자리에 각 페이지의 page.tsx 내용이 들어옵니다.

즉, app/page.tsx가 렌더링되면 그 내용이 {children} 위치에 끼워지는 것입니다.

눈으로 이해하는 children

children이 처음에는 살짝 추상적으로 느껴질 수 있습니다. 그래서 아주 직관적으로 그려 보면 이렇습니다.

layout.tsx가 다음과 같다고 해봅시다.

<body>
  <header>공통 헤더</header>
  <main>{children}</main>
  <footer>공통 푸터</footer>
</body>

그리고 page.tsx가 이렇게 생겼다면,

export default function HomePage() {
  return <section>홈 화면 내용</section>;
}

브라우저에서 보이는 결과는 머릿속으로 이렇게 이해하면 됩니다.

<body>
  <header>공통 헤더</header>
  <main>
    <section>홈 화면 내용</section>
  </main>
  <footer>공통 푸터</footer>
</body>

즉, 레이아웃은 바깥 틀을 만들고, 페이지는 안쪽 내용을 채웁니다.

이 관계를 이해하면 App Router의 절반은 이미 이해한 셈입니다.

이제 홈 화면 page.tsx를 만들어 봅시다

다음은 app/page.tsx입니다. 이 파일은 / 주소, 즉 우리 앱의 첫 화면을 담당합니다.

이번에는 '작은 기록 상점'의 소개와 샘플 기록 몇 개가 보이는 홈 화면으로 바꿔 보겠습니다.

예시 코드는 이렇게 작성할 수 있습니다.

const featuredRecords = [
  {
    id: 'record-1',
    title: '비 오는 날에 적은 한 줄',
    summary: '천천히 걷는 시간이 생각보다 큰 위로가 되었다.',
    category: '일상',
  },
  {
    id: 'record-2',
    title: '읽다 멈춘 책갈피의 문장',
    summary: '좋은 문장은 오래 남고, 오래 남는 문장은 다시 꺼내 보게 된다.',
    category: '독서',
  },
  {
    id: 'record-3',
    title: '오늘 공부에서 건진 작은 발견',
    summary: '한 번에 다 이해하려 하지 말고, 한 줄씩 연결하면 된다.',
    category: '공부',
  },
];

export default function HomePage() {
  return (
    <section>
      <div>
        <p>오늘의 기록 한 조각</p>
        <h2>기록을 진열하듯, 마음에 남는 순간을 모아 보세요</h2>
        <p>
          작은 기록 상점은 일상, 독서, 공부 속에서 건져 올린 짧은 기록들을
          차곡차곡 모아 두는 공간입니다.
        </p>
      </div>

      <section>
        <h3>이런 기록들이 놓일 예정이에요</h3>

        {featuredRecords.map((record) => (
          <article key={record.id}>
            <p>{record.category}</p>
            <h4>{record.title}</h4>
            <p>{record.summary}</p>
          </article>
        ))}
      </section>
    </section>
  );
}

아직 데이터베이스도 없고, API도 없습니다. 그래서 지금은 featuredRecords라는 더미 데이터를 직접 파일 안에 적어 두었습니다.

더미 데이터는 진짜 데이터가 연결되기 전까지 임시로 쓰는 연습용 데이터입니다. 초보자에게는 아주 좋은 친구입니다. 기능을 한 번에 다 만들려 하면 머리가 복잡해지기 쉬운데, 더미 데이터를 쓰면 일단 화면부터 완성할 수 있기 때문입니다.

여기서 어떤 일이 벌어질까요?

방금 만든 두 파일은 함께 동작합니다.

app/layout.tsx는 공통 헤더와 푸터를 만들고, app/page.tsx는 홈 화면의 실제 내용을 만듭니다. 그러면 브라우저에는 이런 흐름으로 화면이 나타납니다.

  • 상단에는 '작은 기록 상점' 제목과 소개

  • 가운데에는 홈 화면 본문

  • 하단에는 푸터

중요한 점은, 홈 화면의 내용만 page.tsx에 있고 바깥 틀은 layout.tsx에 있다는 것입니다.

처음에는 "굳이 나눠야 하나?" 싶을 수 있습니다. 하지만 페이지가 2개, 3개, 10개로 늘어나기 시작하면 이 구조가 엄청나게 편해집니다.

폴더 구조가 URL이 된다는 감각 익히기

이제 App Router의 진짜 재미를 조금 더 느껴 봅시다.

만약 앞으로 기록 목록 화면을 만들고 싶다면, 코드는 대단히 복잡하지 않습니다. app/records/page.tsx 파일을 만들면 됩니다.

구조는 이렇게 됩니다.

app/
  layout.tsx
  page.tsx
  records/
    page.tsx

그러면 자동으로 다음과 같은 주소가 생깁니다.

  • app/page.tsx → /

  • app/records/page.tsx → /records

정말로 이게 끝입니다.

이런 방식이 파일 기반 라우팅입니다. 직접 만져 보면 "아, 그래서 App Router구나" 하는 감각이 옵니다. 주소를 위한 설정 코드보다, 폴더 구조 자체가 더 중요한 세계인 셈이죠.

레이아웃이 공통 구조를 맡는다는 뜻

이번에는 layout.tsx의 장점을 조금 더 현실적으로 생각해 봅시다.

만약 레이아웃 없이 홈, 목록, 상세 페이지를 각각 만든다면, 모든 페이지 파일에 이런 코드를 계속 적어야 할 수 있습니다.

<header>...</header>
<main>...</main>
<footer>...</footer>

한두 번은 괜찮지만, 페이지가 늘어나면 금방 피곤해집니다. 그리고 헤더 제목을 바꾸고 싶을 때 모든 파일을 열어 수정해야 할 수도 있습니다.

하지만 layout.tsx를 사용하면 공통 구조를 한 번만 작성하면 됩니다.

더 반가운 점도 있습니다. App Router에서 레이아웃은 페이지 이동 중에도 공통 부분이 안정적으로 유지되도록 설계되어 있습니다. 즉, 페이지가 바뀌어도 레이아웃은 "앱의 골격"처럼 계속 버텨 줍니다.

초보자 입장에서는 이렇게 기억하면 충분합니다.

페이지는 바뀌어도, 공통 틀은 레이아웃이 잡아 준다.

홈 화면을 조금 더 보기 좋게 다듬기

지금 코드도 동작은 잘 하지만, 구조를 더 읽기 쉽게 정리할 수 있습니다. 예를 들어 main 안쪽을 영역별로 나누면 나중에 스타일을 붙일 때 훨씬 편합니다.

app/page.tsx를 이런 식으로 다듬어 볼 수도 있습니다.

const featuredRecords = [
  {
    id: 'record-1',
    title: '비 오는 날에 적은 한 줄',
    summary: '천천히 걷는 시간이 생각보다 큰 위로가 되었다.',
    category: '일상',
  },
  {
    id: 'record-2',
    title: '읽다 멈춘 책갈피의 문장',
    summary: '좋은 문장은 오래 남고, 오래 남는 문장은 다시 꺼내 보게 된다.',
    category: '독서',
  },
  {
    id: 'record-3',
    title: '오늘 공부에서 건진 작은 발견',
    summary: '한 번에 다 이해하려 하지 말고, 한 줄씩 연결하면 된다.',
    category: '공부',
  },
];

export default function HomePage() {
  return (
    <>
      <section>
        <p>작은 기록 상점에 오신 것을 환영합니다</p>
        <h2>기억하고 싶은 순간을 한 줄씩 모아 두는 곳</h2>
        <p>
          일상의 메모, 읽은 문장, 공부하며 얻은 깨달음을 가볍게 남기고
          다시 꺼내 볼 수 있는 공간을 만들어 봅시다.
        </p>
      </section>

      <section>
        <h3>진열대 미리 보기</h3>
        <p>나중에는 이 자리에 실제 기록 데이터가 채워질 예정입니다.</p>

        {featuredRecords.map((record) => (
          <article key={record.id}>
            <strong>{record.category}</strong>
            <h4>{record.title}</h4>
            <p>{record.summary}</p>
          </article>
        ))}
      </section>
    </>
  );
}

여기서는 특별한 마법이 들어간 것이 없습니다. 단지 화면을 의미 단위로 나누었을 뿐입니다.

이렇게 해 두면 다음 장에서 링크를 붙일 때도 좋고, 나중에 Tailwind CSS로 예쁘게 꾸밀 때도 아주 편합니다.

화면이 어떻게 조립되는지 다시 정리해 봅시다

지금까지 만든 구조를 한 번에 보면 이렇습니다.

app/
  layout.tsx   -> 전체 앱의 공통 틀
  page.tsx     -> / 주소의 홈 화면

동작 흐름은 아래처럼 이해하면 됩니다.

  1. 사용자가 / 주소로 들어온다.

  2. Next.js는 app/page.tsx를 찾는다.

  3. 동시에 app/layout.tsx로 그 페이지를 감싼다.

  4. layout.tsx 안의 {children} 자리에 page.tsx 내용이 들어간다.

  5. 최종 화면이 브라우저에 보인다.

처음에는 단계가 많아 보이지만, 사실 논리는 매우 단순합니다. 바깥은 레이아웃, 안쪽은 페이지입니다.

초보자가 자주 헷갈리는 점

여기서 몇 가지를 미리 정리해 두면 이후 챕터가 훨씬 편해집니다.

폴더를 만들었는데 주소가 안 열려요

대부분은 그 폴더 안에 page.tsx가 없어서 그렇습니다.

예를 들어 app/records/ 폴더만 있고 app/records/page.tsx가 없다면 /records는 열리지 않습니다.

폴더는 주소 조각이고, page.tsx는 실제 공개되는 페이지입니다.

layout.tsx에 꼭 children이 있어야 하나요?

네, 사실상 꼭 필요하다고 생각하면 됩니다.

children이 없으면 레이아웃은 만들었는데 그 안에 페이지 내용이 들어올 자리가 없게 됩니다. 액자는 있는데 그림을 넣을 공간이 없는 셈이죠.

html과 body는 아무 레이아웃에나 넣어도 되나요?

아닙니다. 최상위인 app/layout.tsx, 즉 루트 레이아웃에서만 넣는다고 생각하면 안전합니다.

지금 만든 페이지는 서버에서 돌아가나요, 브라우저에서 돌아가나요?

최신 Next.js의 App Router에서는 기본적으로 서버 컴포넌트 방식으로 시작합니다. 지금처럼 단순히 화면을 보여 주는 page.tsx, layout.tsx는 일단 기본 상태로 충분합니다.

버튼 클릭, 입력 상태 관리 같은 브라우저 상호작용이 필요해지면 그때 클라이언트 컴포넌트를 배웁니다. 이 내용은 뒤에서 자세히 다룰 예정이니, 지금은 "기본은 서버 쪽에서 시작한다" 정도만 기억해 두면 좋습니다.

'작은 기록 상점'에 맞게 생각해 보기

지금 만든 루트 레이아웃은 단순한 예제가 아니라, 앞으로 앱 전체에 계속 깔리는 기초 공사입니다.

예를 들어 앞으로 이런 페이지들이 생길 것입니다.

  • 홈 화면 /

  • 기록 목록 /records

  • 기록 상세 /records/[id]

  • 새 기록 작성 /records/new

이 페이지들은 서로 내용은 다르지만, 같은 상점 안에 있는 화면들입니다. 그래서 앱 이름, 기본 레이아웃, 공통 여백, 공통 헤더 같은 것은 함께 가져가는 것이 자연스럽습니다.

바로 그 역할을 루트 레이아웃이 맡습니다.

즉, 레이아웃은 단지 코드를 줄이기 위한 도구가 아니라, 앱의 세계관을 통일하는 장치이기도 합니다.

조금 거창하게 들릴 수 있지만 정말 그렇습니다. 페이지가 늘어날수록 공통 구조가 앱의 성격을 만들어 주기 때문입니다.

앞으로 중첩 레이아웃도 가능해집니다

이번 장에서는 루트 레이아웃까지만 다루지만, App Router의 진짜 재미 중 하나는 중첩 레이아웃입니다.

예를 들어 나중에 records 영역에만 따로 설명 문구나 사이드 영역을 넣고 싶다면 이런 구조도 가능합니다.

app/
  layout.tsx
  page.tsx
  records/
    layout.tsx
    page.tsx

이 경우 app/layout.tsx는 앱 전체를 감싸고, app/records/layout.tsx는 기록 관련 페이지들만 한 번 더 감싸게 됩니다.

쉽게 말하면 큰 상자 안에 작은 상자를 하나 더 넣는 구조입니다.

이 개념은 다음 챕터들에서 라우팅과 함께 점점 더 빛을 발하게 됩니다. 지금은 "레이아웃은 하나만 있는 것이 아니라, 폴더마다 더 만들 수도 있다" 정도를 가볍게 기억해 두면 충분합니다.

이번 장의 핵심을 손으로 익히는 연습

지금 바로 해 보면 좋은 작은 연습을 소개하겠습니다.

첫 번째 연습은 app/layout.tsx의 헤더 문구를 바꿔 보는 것입니다. 예를 들어 소개 문장을 조금 더 다정하게 고쳐 보세요. 저장하고 브라우저를 새로 보면, 모든 페이지에 공통으로 적용될 준비가 된 구조라는 감각이 생깁니다.

두 번째 연습은 app/page.tsx의 더미 기록을 하나 더 추가해 보는 것입니다. 카드가 늘어나는 모습을 보면, "아, 이건 나중에 목록 데이터로 바뀌겠구나" 하고 자연스럽게 연결됩니다.

세 번째 연습은 app/about/page.tsx 같은 파일을 직접 하나 만들어 보는 것입니다.

export default function AboutPage() {
  return (
    <section>
      <h2>작은 기록 상점 소개</h2>
      <p>기록을 가볍게 남기고 오래 꺼내 보기 위한 연습용 앱입니다.</p>
    </section>
  );
}

그러고 /about에 들어가 보면, 새 페이지가 생겼는데도 헤더와 푸터는 그대로 유지되는 것을 확인할 수 있습니다.

이 순간이 꽤 중요합니다.

"아, 내가 라우터 설정을 안 했는데 페이지가 생겼네?"

바로 그 감각이 App Router를 이해하는 가장 좋은 출발점입니다.

코드보다 더 중요한 이해 한 가지

이번 장에서 꼭 가져가야 할 가장 중요한 이해는 이것입니다.

page.tsx와 layout.tsx는 경쟁하는 파일이 아닙니다. 서로 역할이 다릅니다.

page.tsx는 그 주소의 주인공이고, layout.tsx는 그 주인공이 서 있는 무대입니다.

주인공만 있고 무대가 없으면 장면이 흩어지고, 무대만 있고 주인공이 없으면 보여 줄 것이 없습니다. 둘이 함께 있어야 화면이 자연스럽게 완성됩니다.

이 관계를 이해하면, 이후에 중첩 라우팅을 배우거나 동적 라우트를 배울 때도 훨씬 덜 헷갈립니다.

마무리: 작은 상점의 첫 진열이 시작되다

이번 장에서는 아주 많은 기능을 만들지는 않았습니다. 하지만 가장 중요한 첫 벽돌을 올렸습니다.

우리는 이제 다음 사실을 직접 확인했습니다.

  • app/page.tsx는 특정 주소의 실제 페이지를 만든다.

  • app/layout.tsx는 공통 화면 구조를 만든다.

  • 폴더 구조는 URL 구조와 연결된다.

  • {children} 자리에 페이지가 들어가면서 화면이 조립된다.

이것은 단순한 문법 설명이 아닙니다. 앞으로 만들 기록 목록, 상세 페이지, 작성 페이지까지 모두 이 구조 위에서 자라게 됩니다.

이제 '작은 기록 상점'은 간판도 생겼고, 첫 진열대도 놓였습니다. 다음 장에서는 이 화면들이 서로 오갈 수 있도록 길을 연결해 볼 차례입니다.

정적인 진열장에서, 이제 정말 걸어 다닐 수 있는 웹으로 넘어가 봅시다.

책 목록

처음부터 끝까지 만드는 쉬운 Next.js 활용법

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기