Next.js App Router 첫 프로젝트 만들기

··설명 약 6분·작성: 해비·블로그 소개

책상 정리 용품 · 제휴

모아 보기

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

Next.js App Router에서 page.tsx와 layout.tsx로 첫 페이지를 어떻게 만드나요?

Next.js App Router는 app 폴더의 파일로 URL과 화면을 연결합니다. page.tsx는 해당 경로의 본문, layout.tsx는 공통 UI를 담당합니다.

본문 근거: Next.js Layouts and Pages · Next.js Link

첫 프로젝트 준비와 실행

Node.js와 npm, React 컴포넌트·TypeScript 기본 문법이 필요합니다. 본문 실습은 루트 app/ 구조를 사용합니다. 기존 실행 안내는 Node.js 22 이상을 권장합니다. Next.js가 요구하는 최소 Node.js 버전은 20.9입니다.

TaskFlow ZIP 없이 새 App Router 프로젝트에서 확인할 수 있습니다. 프로젝트가 없다면 다음 명령을 실행하세요.

npx create-next-app@latest route-practice --ts --app
cd route-practice
npm run dev

설정 질문에서 src/ 디렉터리는 사용하지 않는다고 선택하세요. 이미 루트 app/이 있는 프로젝트라면 새 프로젝트를 만들지 않고 그 폴더에서 시작합니다.

바로 확인하기: 터미널에 표시된 개발 서버 주소를 연 뒤 홈·작업 목록 연결 실습에서 두 페이지를 만들고 왕복 이동을 확인합니다. TaskFlow 이전 회차는 이 독립 실습에 필요하지 않습니다.

page.tsx

page.tsx는 해당 route segment의 실제 페이지 UI입니다.

app/page.tsx
→ /

app/tasks/page.tsx
→ /tasks

app/tasks/page.tsx

export default function TasksPage() {
  return (
    <main>
      <h1>작업 목록</h1>
      <p>TaskFlow 작업을 확인합니다.</p>
    </main>
  );
}

layout.tsx

layout은 여러 페이지에서 유지되는 공통 UI를 담당합니다.

import type { ReactNode } from 'react';

export default function RootLayout({
  children,
}: {
  children: ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <header>TaskFlow</header>
        {children}
      </body>
    </html>
  );
}

layout은 페이지 이동 시 공통 UI를 반복 작성하지 않게 해 줍니다.

Link로 이동하기

import Link from 'next/link';

<Link href="/tasks">
  작업 목록
</Link>

일반 a 태그도 링크를 만들 수 있지만 Next.js 내부 이동에는 Link를 사용하는 것이 기본입니다.

폴더 이름이 URL이 됩니다

app/
├─ page.tsx
├─ tasks/
│  └─ page.tsx
└─ settings/
   └─ page.tsx

결과:

/          홈
/tasks     작업 목록
/settings  설정

자주 발생하는 오류

page.tsx가 아닌 임의 파일을 만들었습니다

파일 이름만 만든다고 route가 생성되는 것은 아닙니다. App Router의 페이지 진입점은 page.tsx입니다.

layout.tsx에서 children을 렌더링하지 않았습니다

children이 빠지면 하위 페이지가 화면에 나타나지 않습니다.

React Router 패턴을 그대로 가져옵니다

App Router는 Routes/Route를 작성해서 route를 등록하는 구조가 아닙니다. 폴더 구조 자체가 라우팅 규칙입니다.

독립 실습: 홈과 작업 목록을 서로 연결하기

위의 첫 프로젝트 준비와 실행을 마친 프로젝트에서 시작합니다.

먼저 app/page.tsx를 아래 코드로 바꿉니다.

import Link from 'next/link';

export default function HomePage() {
  return (
    <main>
      <h1>경로 실습</h1>
      <Link href="/tasks">작업 목록 열기</Link>
    </main>
  );
}

이어서 app/tasks/page.tsx를 새로 만듭니다.

import Link from 'next/link';

export default function TasksPage() {
  return (
    <main>
      <h1>작업 목록</h1>
      <Link href="/">홈으로</Link>
    </main>
  );
}

개발 서버 주소의 /에서 ‘작업 목록 열기’를 누르면 /tasks에서 ‘작업 목록’이 보입니다. /tasks를 주소창에서 직접 열거나 새로고침해도 같은 페이지가 보여야 합니다. ‘홈으로’를 누르면 /로 돌아갑니다. 새 프로젝트가 만든 app/layout.tsx는 그대로 둡니다.

스스로 확인: app/tasks/ 폴더를 app/notes/로 바꾸고 홈의 href를 그대로 두면 어떻게 될까요? 링크는 여전히 /tasks를 가리켜 새 /notes 페이지에 도달하지 못합니다. href="/notes"로 맞춰야 합니다. 404라면 폴더 위치, page.tsx 파일명, href를 차례로 확인하세요.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 Next.js로 페이지와 서버 기능을 확장합니다.

홈·작업·설정을 Link로 연결합니다. 작업 데이터 타입과 조회 함수를 분리하여 이후 서버 저장소로 교체하기 쉽게 만듭니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow Next.js 프로젝트 적용

시작 체크포인트: 이전: Next.js App Router 학습 순서와 TaskFlow Next.js 시작의 완료 상태에서 이어갑니다.

이전 회차 완료 ZIP으로 시작하기. 새 폴더에 풀어 현재 회차 완료본과 나란히 비교합니다.

이전 완료본과 달라진 파일: app/_components/SiteHeader.tsx, app/layout.tsx, app/lib/data.ts, app/lib/types.ts, app/page.tsx, app/settings/page.tsx, app/tasks/page.tsx, package.json. 아래 변경 설명을 따라 수정하고 전체 코드의 같은 파일과 대조합니다.

완료 코드 ZIP 다운로드 — next-step-02-app-router-base

이전 체크포인트: next-step-01-first-run

이번 회차의 변경

홈·작업·설정을 Link로 연결합니다. 작업 데이터 타입과 조회 함수를 분리하여 이후 서버 저장소로 교체하기 쉽게 만듭니다.

변경·추가 파일:

  • app/page.tsx
  • app/layout.tsx
  • app/_components/SiteHeader.tsx
  • app/settings/page.tsx
  • app/lib/types.ts
  • app/lib/data.ts
  • app/tasks/page.tsx
전체 파일·코드 보기

실행과 검증

Node.js 22 LTS 이상 권장(Next.js 최소 20.9). 루트 app/ 구조, CSS 사용.

npm install
npm run dev
npm run build
npm run start

전체 코드와 ZIP은 같은 완료 체크포인트입니다. 자동 생성 worker는 ZIP에 포함하며 라이브러리 파일은 코드 뷰어에서 생략합니다.

완료 체크포인트: next-step-02-app-router-base

실습 프로젝트 전체 코드

.gitignore

node_modules
.next
.env*
!.env.example
*.tsbuildinfo

app/_components/SiteHeader.tsx

import Link from "next/link";
export default function SiteHeader() {
  return (
    <header>
      <nav aria-label="주 메뉴">
        <Link href="/">홈</Link>
        <Link href="/tasks">작업</Link>
        <Link href="/settings">설정</Link>
      </nav>
    </header>
  );
}

app/globals.css

* {
  box-sizing: border-box;
}
body {
  margin: 0;
  color: #171717;
  background: #fafafa;
  font-family: Arial, sans-serif;
  line-height: 1.6;
}
header,
main {
  width: min(880px, calc(100% - 32px));
  margin: 24px auto;
}
nav {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
}
a {
  color: inherit;
  text-underline-offset: 3px;
}
button,
input,
select {
  min-height: 44px;
  padding: 8px 12px;
  font: inherit;
  border: 1px solid #aaa;
  background: #fff;
  color: #171717;
  max-width: 100%;
}
button {
  cursor: pointer;
}
button:disabled {
  opacity: 0.6;
  cursor: wait;
}
input {
  display: block;
  width: 100%;
}
label {
  display: block;
}
form,
section,
aside {
  margin: 20px 0;
}
ul {
  padding: 0;
  list-style: none;
}
li {
  padding: 12px;
  margin: 12px 0;
  border: 1px solid #ccc;
  overflow-wrap: anywhere;
}
.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.controls form {
  margin: 0;
}
.modal {
  width: min(640px, calc(100% - 32px));
  border: 1px solid #777;
  padding: 24px;
  background: #fff;
  color: #171717;
}
.modal::backdrop {
  background: rgb(0 0 0 / 55%);
}
:focus-visible {
  outline: 3px solid #555;
  outline-offset: 3px;
}
[data-theme="dark"] {
  background: #202020;
  color: #fafafa;
  padding: 16px;
}
[data-density="compact"] li {
  padding: 8px;
  margin: 8px 0;
}
img {
  max-width: 100%;
  height: auto;
}

app/layout.tsx

import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";
import SiteHeader from "./_components/SiteHeader";
export const metadata: Metadata = {
  title: { default: "TaskFlow Next.js", template: "%s | TaskFlow" },
  description: "서버와 브라우저 역할을 배우는 TaskFlow 학습 프로젝트",
};
export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ko">
      <body>
        <SiteHeader />
        {children}
      </body>
    </html>
  );
}

app/lib/data.ts

import type { Task } from "./types";
const tasks: Task[] = [
  {
    id: "task-1",
    title: "App Router 공부",
    description: "서버와 브라우저의 역할을 구분합니다.",
    completed: false,
    ownerId: "demo",
  },
  {
    id: "task-2",
    title: "TaskFlow 만들기",
    description: "누적 실습을 완성합니다.",
    completed: true,
    ownerId: "demo",
  },
];
export async function getTasks() {
  return tasks;
}
export async function getTask(id: string) {
  return tasks.find((t) => t.id === id);
}
export async function getTaskStats() {
  const ts = await getTasks();
  return { total: ts.length, completed: ts.filter((t) => t.completed).length };
}

app/lib/types.ts

export type Task = {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  ownerId: string;
};

app/page.tsx

import Link from "next/link";
export default function HomePage() {
  return (
    <main>
      <h1>TaskFlow Next.js</h1>
      <p>App Router 기반 작업 관리 서비스입니다.</p>
      <Link href="/tasks">작업 목록 보기</Link>
    </main>
  );
}

app/settings/page.tsx

export default function SettingsPage() {
  return (
    <main>
      <h1>설정</h1>
      <p>TaskFlow 설정 화면입니다.</p>
    </main>
  );
}

app/tasks/page.tsx

import Link from "next/link";
import { getTasks } from "@/app/lib/data";
export default async function TasksPage() {
  const tasks = await getTasks();
  return (
    <main>
      <h1>작업 목록</h1>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            {task.title}
            <span> · {task.completed ? "완료" : "진행 중"}</span>
          </li>
        ))}
      </ul>
    </main>
  );
}

next-env.d.ts

/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";


// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

next.config.ts

import type { NextConfig } from "next";
const config: NextConfig = { cacheComponents: false };
export default config;

package.json

{
  "name": "taskflow-next",
  "version": "1.0.2",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "next": "16.3.5",
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "typescript": "5.9.3",
    "@types/node": "24.13.6",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": [
        "./*"
      ]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".next/dev/types/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

글 소개·학습 준비·전체 목차

하나의 지붕 아래 빈 방 두 개가 있는 나무집
하나의 지붕 아래 빈 방 두 개가 있는 나무집입니다. 실제 동작은 예제 코드에서 확인합니다.

Next.js · 2/38 · TaskFlow 누적 실습

Next.js App Router 첫 프로젝트 만들기

Next.js · 2 / 38 · 필수 학습 · App Router 기본

Next.js App Router의 첫 프로젝트는 create-next-app으로 준비하고, app 디렉터리의 page.tsx와 layout.tsx로 페이지와 공통 UI를 구성합니다. 아래에서 프로젝트를 실행한 뒤 홈과 작업 목록을 Link로 연결하는 흐름을 확인합니다.

이 글에서는 page.tsx와 layout.tsx를 직접 정리하고 Link로 TaskFlow 홈과 작업 목록을 연결합니다.

이번 글에서 배우는 내용

  • app 디렉터리
  • page.tsx
  • layout.tsx
  • 중첩 URL 구조
  • Link
  • 전역 CSS 위치
  • App Router의 기본 라우팅 규칙

목차

다음 학습

다음 글에서는 package.json의 scripts와 dependencies를 읽고 dev·build·start의 역할을 구분합니다.

공식 문서

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 39개 · 필수 38개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기