시작·라우팅

프로젝트를 만들고, 폴더로 주소를 늘리고, 공통 틀과 링크를 붙입니다.

프로젝트 만들기

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에 두는 편이 맞습니다.