Next.js App Router로 만드는 첫 프로젝트 완성 실습 가이드

2026.09.05·수정 2026.09.16·약 19분·작성: 해비·블로그 소개

프로젝트 생성부터 첫 App Router 실습까지 직접 진행합니다

처음 시작한다면 완성 ZIP부터 내려받지 않고 Node.js 확인 → create-next-app → 개발 서버 실행을 먼저 경험합니다. 그다음 홈 → 노트 목록 → 노트 상세 이동을 만들며 파일과 URL의 관계를 확인합니다.

선수 지식: HTML·CSS·JavaScript, React props·state 기초. npm을 처음 본다면 앞선 package.json 학습 글을 함께 확인하세요.

권장 학습 경로: 직접 프로젝트 생성 → 기본 화면 실행 확인 → 실습 ZIP 또는 전체 코드로 예제 구조 확인. 처음부터 완성 파일만 실행하기보다 프로젝트가 어떻게 만들어지는지 먼저 경험하는 것이 좋습니다.

1. 새 Next.js 프로젝트 직접 만들기

먼저 터미널에서 Node.js와 npm이 정상적으로 설치되어 있는지 확인합니다. 이 글의 Next.js 16 기준 최소 Node.js 버전은 20.9 이상입니다.

node -v
npm -v

버전이 정상적으로 출력되면 작업할 폴더에서 새 프로젝트를 만듭니다.

npx create-next-app@latest my-next-app
cd my-next-app
npm run dev

프로젝트 생성 질문에서는 TypeScript와 App Router를 사용하는 구성을 선택하면 이 글의 예제와 연결하기 쉽습니다. 브라우저에서 http://localhost:3000을 열어 기본 Next.js 화면이 보이면 프로젝트 생성과 첫 실행이 완료된 것입니다.

이 단계의 핵심은 명령어를 외우는 것이 아니라 my-next-app이라는 프로젝트 폴더가 생기고, 그 안의 app/page.tsx가 브라우저 화면으로 연결되는 과정을 확인하는 것입니다. app/page.tsx의 제목을 한 번 바꿔 저장하고 화면이 갱신되는지도 확인하세요.

2. 개발 서버와 실습 ZIP 실행하기

위에서 새 프로젝트를 직접 만들어봤다면 이제 이 글의 완성 실습 파일을 비교 자료로 사용할 수 있습니다. ZIP을 사용하는 경우 압축을 풀고 package.json이 있는 폴더에서 다음 명령을 실행합니다. 검증에 사용한 환경은 Node.js 24.19.0·Next.js 16.3.5·React 19.3.0·TypeScript 7.0.2입니다.

npm install
npm run dev

터미널의 로컬 주소를 엽니다. 개발 중 수정 결과를 보는 명령은 dev이며, 마지막에 build·start로 배포용 결과를 확인합니다. 외부 API·로그인·데이터베이스는 필요 없습니다.

직접 만든 프로젝트와 ZIP 프로젝트의 파일 구성이 조금 달라도 문제없습니다. 처음에는 app/page.tsx, app/layout.tsx, app/notes/page.tsx, app/notes/[slug]/page.tsx가 각각 어떤 URL과 연결되는지만 비교하세요.

기본 실습 1: 화면과 파일을 연결합니다

화면 주소 읽을 파일 화면 역할
/ app/page.tsx 홈에서 목록으로 이동
/notes app/notes/page.tsx 두 노트의 링크 표시
/notes/routing app/notes/[slug]/page.tsx routing 노트 상세
모든 화면 app/layout.tsx 공통 내비게이션과 문서 구조

폴더 이름은 URL 구간이 되고 page.tsx가 그 화면을 만듭니다. layout.tsx는 children 자리에 각 화면을 넣습니다. data.ts는 두 노트의 데이터 모듈이며 자체 화면을 만들지 않습니다.

기본 실습 2: 제목을 바꾸고 이동합니다

  1. app/page.tsx의 h1 문구를 바꾸고 홈 화면이 바뀌는지 확인합니다. page.tsx 전체 코드
  2. app/notes/page.tsx에서 Link가 만드는 주소를 확인하고 노트 링크를 엽니다. page.tsx 전체 코드
  3. app/notes/[slug]/page.tsx에서 slug로 해당 노트를 찾는 부분을 읽습니다. params는 await한 뒤 사용합니다. page.tsx 전체 코드
  4. 뒤로 이동해 다른 노트를 열면 제목과 본문은 바뀌고 상단 내비게이션은 유지되는지 확인합니다.

동적 세그먼트 [slug]는 URL의 값이 들어오는 자리입니다. 처음에는 routing과 boundary 두 값만 비교하면 충분합니다.

기본 실습 3: 버튼의 상태만 바꿉니다

app/notes/[slug]/FavoriteButton.tsx의 전체 파일을 확인하세요. useState와 클릭 이벤트를 쓰는 버튼은 파일 첫 줄에 "use client"를 둡니다. page 전체를 클라이언트로 바꾸지 않고 버튼만 분리합니다. FavoriteButton.tsx 전체 코드

즐겨찾기를 누르면 문구와 aria-pressed가 바뀝니다. 새로고침하면 초기화됩니다. 메모리 안의 상태를 연습하는 버튼이며 계정에 저장하는 서비스가 아닙니다.

전체 코드와 파일 선택

기본 학습은 프로젝트 생성 → 홈 → 목록 → 상세 → 버튼 순서입니다. 설정·타입 생성·README 파일은 첫 실행에서 수정하지 않아도 됩니다.

현재 실습 ZIP

현재 본문과 같은 실습 파일 내려받기

README.md

# 실습 8817

Node.js 20.9 이상 (검증: 24.19.0). 이 폴더에서 npm install, npm run build, npm run start. 개발은 npm run dev. 타입 검사는 npm run typecheck.
Next.js 16.3.5 / React 19.3.0 / TypeScript 7.0.2. 외부 DB·인증 서비스는 제공하지 않습니다.
검증: 로컬 production build와 HTTP 검사. 브라우저 조작과 실제 배포는 별도 확인합니다.

app/globals.css

:root {
  font-family: system-ui, sans-serif;
  color: #24211f;
  background: #faf8f4;
}
body {
  margin: 0;
}
header,
main {
  max-width: 720px;
  margin: auto;
  padding: 1.25rem;
}
nav {
  display: flex;
  gap: 1rem;
}
a {
  color: #76533b;
}
.card {
  border: 1px solid #d7cec4;
  border-radius: 12px;
  padding: 1rem;
  margin-block: 1rem;
}
button {
  padding: 0.6rem 1rem;
}

app/layout.tsx

import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";
export const metadata: Metadata = {
  title: "첫 App Router 노트",
  description: "Next.js App Router 첫 프로젝트",
};
export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="ko">
      <body>
        <header>
          <nav>
            <Link href="/">홈</Link>
            <Link href="/notes">노트</Link>
          </nav>
        </header>
        {children}
      </body>
    </html>
  );
}

app/notes/[slug]/FavoriteButton.tsx

"use client";
import { useState } from "react";
export default function FavoriteButton() {
  const [saved, setSaved] = useState(false);
  return (
    <button
      type="button"
      aria-pressed={saved}
      onClick={() => setSaved((value) => !value)}
    >
      {saved ? "즐겨찾기 해제" : "즐겨찾기"}
    </button>
  );
}

app/not-found.tsx

import Link from "next/link";
export default function NotFound() {
  return (
    <main>
      <h1>노트를 찾을 수 없습니다</h1>
      <Link href="/notes">목록으로</Link>
    </main>
  );
}

app/notes/[slug]/page.tsx

import type { Metadata } from "next";
import { notFound } from "next/navigation";
import FavoriteButton from "./FavoriteButton";
import { notes } from "../data";
type Props = { params: Promise<{ slug: string }> };
export const dynamicParams = false;
export function generateStaticParams() {
  return notes.map(({ slug }) => ({ slug }));
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const note = notes.find((x) => x.slug === slug);
  if (!note) notFound();
  return { title: note.title, description: note.body };
}
export default async function NotePage({ params }: Props) {
  const { slug } = await params;
  const note = notes.find((x) => x.slug === slug);
  if (!note) notFound();
  return (
    <main>
      <article>
        <h1>{note.title}</h1>
        <p>{note.body}</p>
        <FavoriteButton />
      </article>
    </main>
  );
}

app/notes/data.ts

export const notes = [
  {
    slug: "routing",
    title: "폴더가 URL이 되는 규칙",
    body: "page.tsx가 경로의 화면을 정의합니다.",
  },
  {
    slug: "boundary",
    title: "서버와 클라이언트 경계",
    body: "상호작용이 필요한 작은 부분만 Client Component로 둡니다.",
  },
] as const;

app/notes/page.tsx

import Link from "next/link";
import { notes } from "./data";
export default function NotesPage() {
  return (
    <main>
      <h1>노트 목록</h1>
      {notes.map((note) => (
        <article className="card" key={note.slug}>
          <h2>{note.title}</h2>
          <Link href={`/notes/${note.slug}`}>자세히 읽기</Link>
        </article>
      ))}
    </main>
  );
}

app/page.tsx

import Link from "next/link";
export default function HomePage() {
  return (
    <main>
      <h1>첫 App Router 프로젝트</h1>
      <p>공유 레이아웃, 목록, 동적 상세, 클라이언트 버튼을 확인합니다.</p>
      <Link href="/notes">노트 목록 열기</Link>
    </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.

package.json

{
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start",
    "typecheck": "next typegen && tsc --noEmit"
  },
  "engines": {
    "node": ">=20.9.0"
  },
  "dependencies": {
    "next": "16.3.5",
    "react": "19.3.0",
    "react-dom": "19.3.0"
  },
  "devDependencies": {
    "typescript": "7.0.2",
    "@types/react": "19.3.0",
    "@types/react-dom": "19.3.0",
    "@types/node": "22.20.2"
  }
}

tsconfig.json

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

기본 실습 완료 기준

  • node -vnpm -v로 실행 환경을 확인했습니다.
  • create-next-app으로 새 프로젝트를 만들고 npm run dev로 기본 화면을 띄웠습니다.
  • 홈 제목을 바꾸고 화면에서 확인했습니다.
  • 목록의 두 링크에서 서로 다른 상세 내용을 확인했습니다.
  • 모든 페이지에서 공통 내비게이션을 확인했습니다.
  • 즐겨찾기 버튼을 누르고 새로고침했을 때의 차이를 설명할 수 있습니다.
npm run typecheck
npm run build
npm run start

dev 서버를 Ctrl+C로 종료한 뒤 start를 실행하세요. 기존 완성본은 빌드와 홈·목록·상세 응답을 검사한 코드입니다. 이번 글 재구성은 실행 파일을 변경하지 않습니다.

추가 실습: 새 노트·검색 제목·404

기본 실습을 마쳤다면 추가 실습 열기

app/notes/data.ts에 고유한 slug, title, body를 가진 노트를 한 건 추가하고 다시 빌드하세요. 목록 링크와 상세 제목이 함께 추가되어야 합니다.

generateStaticParams는 미리 만들 상세 경로를, generateMetadata는 노트별 문서 제목과 설명을 정합니다. 완성본은 dynamicParams=false로 목록 밖의 경로를 닫습니다.

/notes/missing은 HTTP 404와 “노트를 찾을 수 없습니다” 안내를 보여야 합니다. 공통 안내 파일은 app/not-found.tsx입니다. 화면에 오류 문구가 보이는 것과 HTTP 상태가 404인 것을 구분하세요.

기존 비유 이미지

공유 layout과 분리된 페이지를 비유한 이미지입니다. 실제 파일 관계는 위 화면·파일 표로 확인하세요.

하나의 지붕 아래 빈 방 두 개가 있는 나무집
하나의 지붕 아래 빈 방 두 개가 있는 나무집입니다. 실제 동작은 예제 코드에서 확인합니다.
두 공간을 나누는 나무 카운터와 초록색 원반
두 공간을 나누는 나무 카운터와 초록색 원반입니다. 실제 동작은 예제 코드에서 확인합니다.

참고 자료와 다음 학습

공식 자료 재확인: 2026-09-16. 신규 프로젝트 생성과 Node.js 최소 버전은 Next.js 16 App Router 공식 기준으로 확인했습니다.

이 글이 도움이 되었나요?

조회 중

Next.js 학습 순서

필수 13개 · 전체 27개

읽음 기록 관리

전체 과정 목차 (27개)
  1. 필수 길잡이 · Next.js App Router 학습 순서: 설치부터 배포까지
  2. 필수 학습 · Next.js package.json: scripts dependencies 이해
  3. 필수 학습 · Next.js App Router로 만드는 첫 프로젝트 완성 실습 가이드 현재 글
  4. 필수 학습 · Next.js에서 .next 폴더는 어떤 역할을 할까?
  5. 필수 학습 · Next.js 동적 라우트 완전 정리: [slug], params, catch-all
  6. 선택 참고 · Next.js params should be awaited 해결: App Router 기준
  7. 선택 참고 · Next.js window is not defined 오류 해결: 브라우저 API를 안전하게 쓰기
  8. 선택 참고 · Next.js hydration failed 오류 해결: 원인과 해결 방법
  9. 필수 학습 · Axios 사용법: React Next.js에서 API 요청 구조 잡는 법
  10. 선택 참고 · Next.js Route Handler 405 오류 해결: GET/POST 파일 위치와 메서드 설정 확인
  11. 필수 학습 · Next.js Server Actions + React Hook Form 검증 기준
  12. 선택 참고 · Next.js useSearchParams Suspense 오류 해결: 빌드 실패 기준
  13. 선택 참고 · Next.js fetch 캐시 문제 해결: 데이터가 바뀌었는데 화면이 그대로일 때
  14. 선택 참고 · Next.js Dynamic server usage 오류 해결: cookies headers 기준
  15. 선택 참고 · Next.js 환경변수 적용 오류 해결: .env.local을 바꿨는데 값이 안 바뀔 때
  16. 필수 학습 · Next.js Metadata API 완전 정리: 정적 metadata와 generateMetadata
  17. 선택 참고 · Next.js metadata가 적용되지 않을 때 확인할 7가지
  18. 선택 참고 · Next.js Image remotePatterns 오류 해결: 외부 이미지 도메인 허용하기
  19. 필수 학습 · Next.js redirects 설정: next.config.js에서 URL 이동 처리
  20. 필수 학습 · Next.js SEO 체크리스트: metadata·초기 HTML·OG 이미지 점검
  21. 선택 참고 · Next.js SEO 완전 가이드: App Router metadata부터 배포 확인까지
  22. 필수 학습 · Next.js 16 성능 최적화 체크리스트: 번들·이미지·캐시·배포
  23. 필수 학습 · Next.js 렌더링 성능 최적화: React 화면이 느릴 때 기준
  24. 필수 학습 · Next.js 16 Proxy 마이그레이션: Node.js Runtime·matcher 검증
  25. 선택 참고 · Next.js 보안 패치 기준: v16.2.5 영향 범위 점검
  26. 선택 참고 · Next.js SEO SSR 적용법: 검색 노출과 렌더링 구조 잡기
  27. 시점·기록 · Next.js 16.3.0-canary.106의 useCache deprecation 경고와 hybrid not-found 수정 이해하기

새 글 받아보기

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

RSS 피드 구독하기

댓글 남기기