프로젝트 생성부터 첫 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: 제목을 바꾸고 이동합니다
app/page.tsx의 h1 문구를 바꾸고 홈 화면이 바뀌는지 확인합니다. page.tsx 전체 코드app/notes/page.tsx에서 Link가 만드는 주소를 확인하고 노트 링크를 엽니다. page.tsx 전체 코드app/notes/[slug]/page.tsx에서 slug로 해당 노트를 찾는 부분을 읽습니다. params는 await한 뒤 사용합니다. page.tsx 전체 코드- 뒤로 이동해 다른 노트를 열면 제목과 본문은 바뀌고 상단 내비게이션은 유지되는지 확인합니다.
동적 세그먼트 [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 -v와npm -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과 분리된 페이지를 비유한 이미지입니다. 실제 파일 관계는 위 화면·파일 표로 확인하세요.


참고 자료와 다음 학습
- Next.js App Router 학습 순서: 설치부터 배포까지
- Next.js package.json: scripts dependencies 이해
- React 제어 컴포넌트 폼과 상태 끌어올리기 첫 실습 가이드
- Next.js 설치 공식 문서
- 공식 layouts와 pages 안내
- https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes
- Link API
- https://nextjs.org/docs/app/api-reference/functions/generate-metadata
공식 자료 재확인: 2026-09-16. 신규 프로젝트 생성과 Node.js 최소 버전은 Next.js 16 App Router 공식 기준으로 확인했습니다.
이 글이 도움이 되었나요?
Next.js 학습 순서
필수 13개 · 전체 27개
읽음 기록 관리
전체 과정 목차 (27개)
- 필수 길잡이 · Next.js App Router 학습 순서: 설치부터 배포까지
- 필수 학습 · Next.js package.json: scripts dependencies 이해
- 필수 학습 · Next.js App Router로 만드는 첫 프로젝트 완성 실습 가이드 현재 글
- 필수 학습 · Next.js에서 .next 폴더는 어떤 역할을 할까?
- 필수 학습 · Next.js 동적 라우트 완전 정리: [slug], params, catch-all
- 선택 참고 · Next.js params should be awaited 해결: App Router 기준
- 선택 참고 · Next.js window is not defined 오류 해결: 브라우저 API를 안전하게 쓰기
- 선택 참고 · Next.js hydration failed 오류 해결: 원인과 해결 방법
- 필수 학습 · Axios 사용법: React Next.js에서 API 요청 구조 잡는 법
- 선택 참고 · Next.js Route Handler 405 오류 해결: GET/POST 파일 위치와 메서드 설정 확인
- 필수 학습 · Next.js Server Actions + React Hook Form 검증 기준
- 선택 참고 · Next.js useSearchParams Suspense 오류 해결: 빌드 실패 기준
- 선택 참고 · Next.js fetch 캐시 문제 해결: 데이터가 바뀌었는데 화면이 그대로일 때
- 선택 참고 · Next.js Dynamic server usage 오류 해결: cookies headers 기준
- 선택 참고 · Next.js 환경변수 적용 오류 해결: .env.local을 바꿨는데 값이 안 바뀔 때
- 필수 학습 · Next.js Metadata API 완전 정리: 정적 metadata와 generateMetadata
- 선택 참고 · Next.js metadata가 적용되지 않을 때 확인할 7가지
- 선택 참고 · Next.js Image remotePatterns 오류 해결: 외부 이미지 도메인 허용하기
- 필수 학습 · Next.js redirects 설정: next.config.js에서 URL 이동 처리
- 필수 학습 · Next.js SEO 체크리스트: metadata·초기 HTML·OG 이미지 점검
- 선택 참고 · Next.js SEO 완전 가이드: App Router metadata부터 배포 확인까지
- 필수 학습 · Next.js 16 성능 최적화 체크리스트: 번들·이미지·캐시·배포
- 필수 학습 · Next.js 렌더링 성능 최적화: React 화면이 느릴 때 기준
- 필수 학습 · Next.js 16 Proxy 마이그레이션: Node.js Runtime·matcher 검증
- 선택 참고 · Next.js 보안 패치 기준: v16.2.5 영향 범위 점검
- 선택 참고 · Next.js SEO SSR 적용법: 검색 노출과 렌더링 구조 잡기
- 시점·기록 · Next.js 16.3.0-canary.106의 useCache deprecation 경고와 hybrid not-found 수정 이해하기
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.