프로젝트를 만들고, 폴더로 주소를 늘리고, 공통 틀과 링크를 붙입니다.
Node.js 20.9 이상에서 아래를 실행합니다. 질문에는 권장 기본값(TypeScript, ESLint, Tailwind CSS, App Router, Turbopack)을 골라도 됩니다.
npx create-next-app@latest my-app
cd my-app
npm run dev
브라우저에서 http://localhost:3000을 엽니다. 질문을 건너뛰려면 같은 명령 끝에 --yes를 붙입니다.
아래 예제는 JavaScript입니다. 기본값인 TypeScript를 골랐다면 확장자를 .tsx로 두고, 에디터가 알려 주는 타입만 더하면 됩니다. 동작은 같습니다.
| 명령 | 할 일 |
|---|---|
npm run dev |
개발 서버. 파일을 저장하면 화면이 갱신됩니다. 번들러 기본값은 Turbopack입니다. |
npm run build |
배포용으로 빌드합니다. 타입·라우트 오류가 여기서 드러나는 경우가 많습니다. |
npm start |
빌드 결과를 프로덕션 모드로 실행합니다. dev 대신 이 명령으로 최종 확인합니다. |
npm run lint |
ESLint로 코드 문제를 검사합니다. |
my-app/
app/
layout.tsx 모든 페이지를 감싸는 틀. html, body는 여기만
page.tsx 주소 /
globals.css
public/ 이미지 등 정적 파일. 주소는 /파일명
next.config.ts Next.js 설정
package.json 의존성과 명령
tsconfig.json TypeScript 설정
src/ 폴더를 쓰도록 골랐다면 app은 src/app에 있습니다. URL은 달라지지 않습니다. public/hero.png의 주소는 /hero.png입니다.
app/page.js가 홈입니다. 기본으로 내보내는 함수가 그 주소의 화면입니다.
export default function Page() {
return <h1>Hello Next.js</h1>
}
app/blog/page.js를 추가하면 /blog가 생깁니다.
export default function Page() {
return <h1>글 목록</h1>
}
루트 app/layout.js는 모든 페이지를 감쌉니다. html과 body는 루트 레이아웃에만 둡니다. 다른 주소로 이동해도 레이아웃은 상태를 유지하고 다시 그리지 않습니다.
export default function RootLayout({ children }) {
return (
<html lang="ko">
<body>{children}</body>
</html>
)
}
app/blog/layout.js를 두면 /blog와 그 하위 주소만 한 번 더 감쌉니다. 여기에는 html, body를 넣지 않습니다.
export default function BlogLayout({ children }) {
return (
<>
<p>블로그</p>
{children}
</>
)
}
같은 사이트 안의 이동은 Link를 씁니다. 일반 <a href>는 페이지 전체를 새로 받아 레이아웃 상태가 사라집니다.
import Link from 'next/link'
export default function Page() {
return (
<p>
<Link href="/blog">글 목록</Link>
</p>
)
}
바깥 사이트는 그냥 <a>를 씁니다. Link의 href는 문자열 "/blog"처럼 적습니다.
폴더 이름을 대괄호로 감싸면 그 자리가 매개변수입니다. app/blog/[slug]/page.js는 /blog/hello, /blog/next를 모두 받습니다.
최근 Next.js에서 params는 Promise입니다. await로 꺼냅니다.
export default async function Page({ params }) {
const { slug } = await params
return <h1>{slug}</h1>
}
검색어 ?q=next는 searchParams입니다. 이것도 Promise입니다.
export default async function Page({ searchParams }) {
const query = await searchParams
const q = query.q ?? ''
return <p>검색어: {q}</p>
}
| 폴더 | 맞는 주소 |
|---|---|
[slug] |
조각 하나. /blog/hello |
[...slug] |
하나 이상. /shop/a, /shop/a/b |
[[...slug]] |
없어도 됨. /docs와 /docs/a/b |
같은 폴더의 특수 파일이 그 주소의 상태를 담당합니다. 모두 선택입니다.
loading.js: 이 구간을 만드는 동안 보여 줄 화면. 안에서는 Suspense로 페이지를 감쌉니다.not-found.js: notFound()를 호출했을 때의 404 화면.error.js: 이 구간에서 난 오류. 반드시 클라이언트 컴포넌트('use client')입니다.export default function Loading() {
return <p>불러오는 중</p>
}
loading.js는 같은 폴더의 page를 감쌉니다. 레이아웃 안에서 쿠키·헤더를 읽으면 그 로딩 화면이 대신 나오지 않고, 레이아웃이 끝날 때까지 이동이 멈춥니다. 느린 데이터는 페이지 쪽이나, 데이터 바로 근처의 Suspense에 두는 편이 맞습니다.