App Router#
- 상위 메뉴: Migrating
- 전체 목차: Next.js 학습 문서
학습 목표#
- Next.js 12에서 13으로 올릴 때 필요한 Node.js, Next.js, ESLint 변경을 적용한다.
pages와app디렉터리를 함께 유지하며 화면을 점진적으로 옮긴다.- 페이지, 라우팅 훅, 데이터 fetching, API Route, 스타일의 이전 규칙을 구분한다.
핵심 개념 및 설명#
업그레이드#
Node.js Version#
최소 Node.js 버전은 v18.17이다. 자세한 내용은 Node.js 문서를 참고한다.
Next.js Version#
Next.js 13으로 올리려면 사용하는 패키지 관리 도구로 다음 명령을 실행한다.
pnpm add next@latest react@latest react-dom@latestESLint Version#
ESLint를 사용한다면 ESLint 버전도 올려야 한다.
pnpm add -D eslint-config-next@latest알아두면 좋은 점: ESLint 변경을 반영하려면 VS Code의 ESLint 서버를 다시 시작해야 할 수 있다. Command Palette(Mac은cmd+shift+p, Windows는ctrl+shift+p)를 열어ESLint: Restart ESLint Server를 검색한다.
다음 단계#
업그레이드를 마쳤다면 다음 단계를 참고한다.
- 새 기능 올리기: 개선된 Image, Link 컴포넌트 같은 새 기능으로 올리는 안내다.
- `pages`에서 `app` 디렉터리로 옮기기:
pages에서app디렉터리로 점진적으로 옮기는 단계별 안내다.
새 기능 올리기#
Next.js 13은 새 기능과 컨벤션을 가진 새 App Router를 도입했다. 새 라우터는 app 디렉터리에서 쓸 수 있으며 pages 디렉터리와 공존한다.
Next.js 13으로 올린다고 App Router를 반드시 써야 하는 것은 아니다. 개선된 `<Image>` 컴포넌트, `<Link>` 컴포넌트, `<Script>` 컴포넌트, Font Optimization처럼 두 디렉터리에서 쓸 수 있는 새 기능은 pages를 계속 쓰면서도 채택할 수 있다.
<Image> Component#
Next.js 12의 next/future/image에 있던 적은 클라이언트 JavaScript, 쉬운 스타일 확장, 접근성 개선, 브라우저 기본 지연 로딩 동작이 Next.js 13에서는 next/image의 기본값이 됐다. 기존 동작을 그대로 유지하려면 next-image-to-legacy-image codemod로 import를 next/legacy/image로 바꾼다. 새 동작으로 옮기는 next-image-experimental은 인라인 스타일을 추가하고 사용하지 않는 prop을 제거하므로 동작이 달라질 수 있다. 이 변환은 먼저 legacy 변환을 실행해야 한다.
<Link> Component#
<Link> 안에 <a>를 직접 넣을 필요가 없다. 이 동작은 버전 12.2에서 실험적 옵션으로 추가됐고 이제 기본값이 됐다. Next.js 13의 <Link>는 항상 내부적으로 <a>를 렌더링하고, 해당 태그로 prop을 전달할 수 있다. new-link codemod로 기존 형식을 바꿀 수 있다.
import Link from 'next/link'
// Next.js 12: <a>를 자식으로 넣지 않으면 제외된다
<Link href="/about">
<a>About</a>
</Link>
// Next.js 13: <Link>가 내부적으로 항상 <a>를 렌더링한다
<Link href="/about">
About
</Link><Script> Component#
`next/script`의 동작이 pages와 app 모두를 지원하도록 갱신됐지만, 매끄러운 이전을 위해 몇 가지를 바꿔야 한다.
- 이전에
_document.js에 두었던beforeInteractive스크립트는app/layout.tsx같은 Root Layout으로 옮긴다. - 실험적
worker전략은app에서 아직 동작하지 않으므로, 이 전략을 쓰던 스크립트는 제거하거나lazyOnload같은 다른 전략으로 바꿔야 한다. onLoad,onReady,onError핸들러는 Server Component에서 동작하지 않으므로 Client Component로 옮기거나 완전히 제거한다.
Font Optimization#
이전에는 Next.js가 폰트 CSS를 인라인하는 방식으로 폰트 최적화를 도왔다. Version 13은 새 `next/font` 모듈을 도입해, 뛰어난 성능과 프라이버시를 유지하면서 폰트 로딩 경험을 커스터마이즈할 수 있게 한다. next/font는 pages와 app 디렉터리 모두에서 지원한다.
CSS 인라인 방식은 pages에서는 계속 동작하지만 app에서는 동작하지 않는다. 대신 next/font를 사용해야 한다.
pages에서 app으로 옮기기#
App Router로 옮기는 것은 Next.js가 기반으로 삼는 Server Component, Suspense 같은 React 기능을 처음 써보는 경험일 수 있다. 특수 파일, 레이아웃 같은 새 Next.js 기능과 결합되면, 마이그레이션에는 새 개념·멘탈 모델·동작 변화를 배우는 과정이 함께 따른다.
이런 변화를 한 번에 배우는 대신, 이전을 더 작은 단계로 나누는 것을 권장한다. app 디렉터리는 pages 디렉터리와 동시에 동작하도록 의도적으로 설계되어 있어 페이지 단위로 점진적으로 옮길 수 있다.
app디렉터리는 중첩 라우트와 레이아웃을 지원한다.- 중첩 폴더로 라우트를 정의하고, 특수
page.js파일로 라우트 세그먼트를 공개적으로 접근 가능하게 만든다. - 특수 파일 규칙으로 각 라우트 세그먼트의 UI를 만든다. 가장 흔한 특수 파일은
page.js와layout.js다.page.js는 라우트별 UI를,layout.js는 여러 라우트가 공유하는 UI를 만든다. 특수 파일에는.js,.jsx,.tsx를 쓸 수 있다. - 컴포넌트, 스타일, 테스트 파일 등은
app안에 함께 둘 수 있다. getServerSideProps,getStaticProps같은 데이터 fetching 함수는app안의 새 API로 대체됐다.getStaticPaths는 `generateStaticParams`로 대체됐다.pages/_app.js와pages/_document.js는app/layout.js하나의 Root Layout으로 대체됐다.pages/_error.js는 더 세분화된error.js특수 파일로 대체됐다.pages/404.js는 `not-found.js` 파일로 대체됐다.pages/api/*API Route는 `route.js`(Route Handler) 특수 파일로 대체됐다.
1단계: app 디렉터리 만들기#
최신 Next.js 버전(13.4 이상 필요)으로 올린다.
pnpm add next@latest그 뒤 프로젝트 루트(또는 src/ 디렉터리)에 새 app 디렉터리를 만든다.
2단계: Root Layout 만들기#
app 디렉터리 안에 새 app/layout.tsx 파일을 만든다. 이것이 app 안 모든 라우트에 적용되는 Root Layout이다.
export default function RootLayout({
// 레이아웃은 children prop을 받아야 한다.
// 여기에 중첩 레이아웃이나 페이지가 채워진다
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}app디렉터리에는 Root Layout이 반드시 있어야 한다.- Next.js가 자동으로 만들지 않으므로 Root Layout은
<html>,<body>태그를 직접 정의해야 한다. - Root Layout은
pages/_app.tsx와pages/_document.tsx파일을 대체한다. - 레이아웃 파일에는
.js,.jsx,.tsx확장자를 쓸 수 있다.
<head> HTML 요소를 관리하려면 내장 SEO 지원을 사용한다.
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Home',
description: 'Welcome to Next.js',
}##### _document.js와 _app.js 옮기기
기존 _app이나 _document 파일이 있다면 그 내용(예: 전역 스타일)을 Root Layout(app/layout.tsx)으로 복사할 수 있다. app/layout.tsx의 스타일은 pages/*에 적용되지 않는다. pages/* 라우트가 깨지지 않도록 이전 중에는 _app/_document를 유지해야 하며, 완전히 옮긴 뒤에는 안전하게 지울 수 있다.
React Context Provider를 쓰고 있다면 Client Component로 옮겨야 한다.
##### getLayout() 패턴을 레이아웃으로 옮기기 (선택)
Next.js는 pages 디렉터리에서 페이지별 레이아웃을 구현하려고 Page 컴포넌트에 속성을 추가하는 방식을 권장했다. 이 패턴은 app 디렉터리의 중첩 레이아웃 내장 지원으로 대체할 수 있다.
전후 예시:
// components/DashboardLayout.js — Before
export default function DashboardLayout({ children }) {
return (
<div>
<h2>My Dashboard</h2>
{children}
</div>
)
}// pages/dashboard/index.js — Before
import DashboardLayout from '../components/DashboardLayout'
export default function Page() {
return <p>My Page</p>
}
Page.getLayout = function getLayout(page) {
return <DashboardLayout>{page}</DashboardLayout>
}pages/dashboard/index.js의 Page.getLayout 속성을 지우고 4단계(페이지 옮기기)의 절차를 따른다.
// app/dashboard/page.js — After
export default function Page() {
return <p>My Page</p>
}DashboardLayout의 내용을 새 Client Component로 옮겨 pages 디렉터리와 같은 동작을 유지한다.
// app/dashboard/DashboardLayout.js — After
'use client' // 이 지시어는 모든 import보다 앞, 파일 맨 위에 있어야 한다
// Client Component다
export default function DashboardLayout({ children }) {
return (
<div>
<h2>My Dashboard</h2>
{children}
</div>
)
}새 layout.js 파일에서 DashboardLayout을 app 디렉터리로 import한다.
// app/dashboard/layout.js — After
import DashboardLayout from './DashboardLayout'
// Server Component다
export default function Layout({ children }) {
return <DashboardLayout>{children}</DashboardLayout>
}클라이언트로 보내는 컴포넌트 JavaScript 양을 줄이려면, DashboardLayout.js(Client Component)의 인터랙티브하지 않은 부분을 layout.js(Server Component)로 점진적으로 옮길 수 있다.
3단계: next/head 옮기기#
pages 디렉터리에서는 next/head의 React 컴포넌트로 title, meta 같은 <head> HTML 요소를 관리한다. app 디렉터리에서는 next/head가 새 내장 SEO 지원으로 대체된다.
// pages/index.tsx — Before
import Head from 'next/head'
export default function Page() {
return (
<>
<Head>
<title>My page title</title>
</Head>
</>
)
}// app/page.tsx — After
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My Page Title',
}
export default function Page() {
return '...'
}전체 메타데이터 옵션은 `generateMetadata`에서 확인한다.
4단계: 페이지 옮기기#
- `app` 디렉터리의 페이지는 기본적으로 Server Component다.
pages에서는 페이지가 Client Component라는 점과 다르다. app에서는 데이터 fetching이 바뀌었다.getServerSideProps,getStaticProps,getInitialProps는 더 단순한 API로 대체됐다.app디렉터리는 중첩 폴더로 라우트를 정의하고, 특수page.js파일로 라우트 세그먼트를 공개적으로 접근 가능하게 만든다.
pages 디렉터리 | app 디렉터리 | 라우트 |
|---|---|---|
index.js | page.js | / |
about.js | about/page.js | /about |
blog/[slug].js | blog/[slug]/page.js | /blog/post-1 |
페이지 이전은 두 단계로 나누는 것을 권장한다.
- 1단계: 기본 export된 Page 컴포넌트를 새 Client Component로 옮긴다.
- 2단계: 새 Client Component를
app디렉터리의 새page.js파일에서 import한다.
알아두면 좋은 점: 이 방식이 가장 쉬운 마이그레이션 경로다. pages 디렉터리와 가장 비슷한 동작을 유지하기 때문이다.1단계: 새 Client Component 만들기
app디렉터리 안에 Client Component를 export하는 새 파일(예:app/home-page.tsx)을 만든다. Client Component를 정의하려면 파일 맨 위(모든 import보다 앞)에'use client'지시어를 추가한다.- Pages Router와 비슷하게, 첫 페이지 로드에서 Client Component를 정적 HTML로 prerender하는 최적화 단계가 있다.
pages/index.js에서 기본 export된 페이지 컴포넌트를app/home-page.tsx로 옮긴다.
'use client'
// Client Component다(pages 디렉터리의 컴포넌트와 같다)
// props로 데이터를 받고, state와 effect에 접근할 수 있으며,
// 첫 페이지 로드 중 서버에서 prerender된다.
export default function HomePage({ recentPosts }) {
return (
<div>
{recentPosts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}2단계: 새 페이지 만들기
app디렉터리 안에 새app/page.tsx파일을 만든다. 기본적으로 Server Component다.home-page.tsxClient Component를 이 페이지에서 import한다.pages/index.js에서 데이터를 가져오고 있었다면, 데이터 fetching 로직을 새 데이터 fetching API를 사용해 Server Component 안으로 직접 옮긴다. 자세한 내용은 6단계를 참고한다.
// Client Component를 import한다
import HomePage from './home-page'
async function getPosts() {
const res = await fetch('https://...')
const posts = await res.json()
return posts
}
export default async function Page() {
// Server Component에서 데이터를 직접 가져온다
const recentPosts = await getPosts()
// 가져온 데이터를 Client Component로 전달한다
return <HomePage recentPosts={recentPosts} />
}- 이전 페이지가
useRouter를 사용했다면 새 라우팅 훅으로 갱신해야 한다. 자세히 보기. - 개발 서버를 실행하고
http://localhost:3000에 방문한다.app디렉터리를 통해 제공되는 기존 index 라우트가 보인다.
5단계: 라우팅 훅 옮기기#
app 디렉터리의 새 동작을 지원하는 새 라우터가 추가됐다. app에서는 next/navigation에서 가져오는 세 가지 새 훅 — `useRouter()`, `usePathname()`, `useSearchParams()` — 를 사용해야 한다.
- 새
useRouter훅은next/navigation에서 가져오며,pages의useRouter(이 훅은next/router에서 가져온다)와 동작이 다르다. next/router에서 가져오는useRouter훅은app디렉터리에서 지원하지 않지만,pages디렉터리에서는 계속 쓸 수 있다.- 새
useRouter는pathname문자열을 반환하지 않는다. 대신 별도의usePathname훅을 사용한다. - 새
useRouter는query객체를 반환하지 않는다. 검색 파라미터와 다이나믹 라우트 파라미터가 이제 분리됐다.useSearchParams와useParams훅을 사용한다. useSearchParams와usePathname을 함께 써서 페이지 변경을 감지할 수 있다.- 이 새 훅들은 Client Component에서만 지원한다. Server Component에서는 쓸 수 없다.
'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
export default function ExampleClientComponent() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
// ...
}추가로 새 useRouter 훅에는 다음과 같은 변경도 있다.
isFallback이 제거됐다.fallback이 대체됐기 때문이다.locale,locales,defaultLocales,domainLocales값이 제거됐다.app디렉터리에서는 내장 i18n 기능이 더 이상 필요하지 않기 때문이다. i18n 더 알아보기.basePath가 제거됐다. 대안은useRouter의 일부로 제공되지 않으며, 아직 구현되지 않았다.asPath가 제거됐다.as개념이 새 라우터에서 없어졌기 때문이다.isReady가 제거됐다. 더 이상 필요하지 않기 때문이다. prerendering 중 `useSearchParams()` 훅을 쓰는 컴포넌트는 prerendering 단계를 건너뛰고 런타임에 클라이언트에서 렌더링된다.route가 제거됐다.usePathname이나useSelectedLayoutSegments()가 대안이다.
##### pages와 app 사이에 컴포넌트 공유하기
pages와 app 라우터 사이에서 컴포넌트를 호환되게 유지하려면 next/compat/router의 useRouter 훅을 참고한다. 이는 pages 디렉터리의 useRouter 훅이지만, 두 라우터 사이에 컴포넌트를 공유하는 동안 쓰도록 만들어졌다. app 라우터에서만 쓸 준비가 되면 next/navigation의 새 useRouter로 갱신한다.
6단계: 데이터 fetching 방식 옮기기#
pages 디렉터리는 getServerSideProps와 getStaticProps로 페이지 데이터를 가져온다. app 디렉터리에서는 이 데이터 fetching 함수들이 `fetch()`와 async React Server Component 위에 지은 더 단순한 API로 대체된다.
export default async function Page() {
// 이 요청에 Next.js Data Cache를 적용한다.
// 캐시된 응답은 여러 요청에서 재사용되고 필요에 따라 revalidate된다.
// getStaticProps와 비슷하다.
const cachedData = await fetch('https://...', { cache: 'force-cache' })
// 이 요청은 캐시하지 않는다.
// Next.js는 매 요청마다 데이터 소스에서 가져온다.
// 기본 fetch 동작이며, getServerSideProps와 비슷하다.
const uncachedData = await fetch('https://...', { cache: 'no-store' })
// 이 요청은 캐시하되 최소 10초마다 revalidate한다.
// revalidate 옵션이 있는 getStaticProps와 비슷하다.
const revalidatedData = await fetch('https://...', {
next: { revalidate: 10 },
})
return <div>...</div>
}##### 서버 사이드 렌더링(getServerSideProps)
pages 디렉터리에서는 getServerSideProps로 서버에서 데이터를 가져와 파일의 기본 export 컴포넌트로 prop을 전달한다. 페이지의 초기 HTML은 서버에서 prerender된 뒤 브라우저에서 hydrate돼 인터랙티브해진다.
// pages/dashboard.js — `pages` 디렉터리
export async function getServerSideProps() {
const res = await fetch(`https://...`)
const projects = await res.json()
return { props: { projects } }
}
export default function Dashboard({ projects }) {
return (
<ul>
{projects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
)
}App Router에서는 Server Component를 사용해 데이터 fetching을 React 컴포넌트 안에 함께 둘 수 있다. 이렇게 하면 서버에서 렌더링된 HTML은 유지하면서 클라이언트로 보내는 JavaScript는 줄어든다.
cache 옵션을 no-store로 설정하면 가져온 데이터를 절대 캐시하지 않도록 지시할 수 있다. pages 디렉터리의 getServerSideProps와 비슷하다.
// app/dashboard/page.tsx — `app` 디렉터리
// 이 함수는 이름을 자유롭게 지을 수 있다
async function getProjects() {
const res = await fetch(`https://...`, { cache: 'no-store' })
const projects = await res.json()
return projects
}
export default async function Dashboard() {
const projects = await getProjects()
return (
<ul>
{projects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
)
}##### 요청 객체 접근하기
pages 디렉터리에서는 Node.js HTTP API를 바탕으로 요청 기반 데이터를 가져올 수 있다. 예를 들어 getServerSideProps에서 req 객체를 받아 요청의 쿠키와 헤더를 읽을 수 있다.
// pages/index.js — `pages` 디렉터리
export async function getServerSideProps({ req, query }) {
const authHeader = req.getHeaders()['authorization']
const theme = req.cookies['theme']
return { props: { /* ... */ } }
}
export default function Page(props) {
return /* ... */
}app 디렉터리는 요청 데이터를 가져오는 새 읽기 전용 함수를 제공한다.
- `headers`: Web Headers API를 바탕으로 하며, Server Component 안에서 요청 헤더를 가져올 때 쓴다.
- `cookies`: Web Cookies API를 바탕으로 하며, Server Component 안에서 쿠키를 가져올 때 쓴다.
// app/page.tsx — `app` 디렉터리
import { cookies, headers } from 'next/headers'
async function getData() {
const authHeader = (await headers()).get('authorization')
return '...'
}
export default async function Page() {
// Server Component 안에서 직접, 또는 데이터 fetching 함수 안에서
// cookies나 headers를 쓸 수 있다
const theme = (await cookies()).get('theme')
const data = await getData()
return '...'
}##### 정적 사이트 생성(getStaticProps)
pages 디렉터리에서는 getStaticProps 함수로 빌드 시점에 페이지를 prerender한다. 이 함수는 외부 API나 데이터베이스에서 데이터를 가져와 빌드되는 동안 페이지 전체에 전달할 수 있다.
// pages/index.js — `pages` 디렉터리
export async function getStaticProps() {
const res = await fetch(`https://...`)
const projects = await res.json()
return { props: { projects } }
}
export default function Index({ projects }) {
return projects.map((project) => <div>{project.name}</div>)
}app 디렉터리에서는 `fetch()`로 데이터를 가져올 때 기본값이 cache: 'force-cache'라서 수동으로 무효화하기 전까지 요청 데이터가 캐시된다. pages 디렉터리의 getStaticProps와 비슷하다.
// app/page.js — `app` 디렉터리
// 이 함수는 이름을 자유롭게 지을 수 있다
async function getProjects() {
const res = await fetch(`https://...`)
const projects = await res.json()
return projects
}
export default async function Index() {
const projects = await getProjects()
return projects.map((project) => <div>{project.name}</div>)
}##### 다이나믹 경로(getStaticPaths)
pages 디렉터리에서는 getStaticPaths 함수로 빌드 시점에 prerender할 다이나믹 경로를 정의한다.
// pages/posts/[id].js — `pages` 디렉터리
import PostLayout from '@/components/post-layout'
export async function getStaticPaths() {
return {
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
}
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://.../posts/${params.id}`)
const post = await res.json()
return { props: { post } }
}
export default function Post({ post }) {
return <PostLayout post={post} />
}app 디렉터리에서는 getStaticPaths가 `generateStaticParams`로 대체된다.
`generateStaticParams`는 getStaticPaths와 비슷하게 동작하지만, 라우트 파라미터를 반환하는 더 단순한 API를 가지고 있고 레이아웃 안에서도 쓸 수 있다. generateStaticParams의 반환 형태는 중첩된 param 객체의 배열이나 해석된 경로 문자열이 아니라, 세그먼트로 이루어진 배열이다.
// app/posts/[id]/page.js — `app` 디렉터리
import PostLayout from '@/components/post-layout'
export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }]
}
async function getPost(params) {
const res = await fetch(`https://.../posts/${(await params).id}`)
const post = await res.json()
return post
}
export default async function Post({ params }) {
const post = await getPost(params)
return <PostLayout post={post} />
}app 디렉터리의 새 모델에는 getStaticPaths보다 generateStaticParams라는 이름이 더 적합하다. get 접두사는 더 설명적인 generate로 바뀌었는데, getStaticProps와 getServerSideProps가 더 이상 필요 없어진 지금 이 접두사가 홀로 있어도 더 잘 어울린다. Paths 접미사는 Params로 바뀌었는데, 여러 다이나믹 세그먼트가 있는 중첩 라우팅에 더 적합한 표현이기 때문이다.
##### fallback 대체하기
pages 디렉터리에서는 getStaticPaths가 반환하는 fallback 속성으로 빌드 시점에 prerender되지 않은 페이지의 동작을 정의한다. 이 속성을 true로 설정하면 페이지가 생성되는 동안 fallback 페이지를 보여주고, false로 설정하면 404 페이지를 보여주며, blocking으로 설정하면 요청 시점에 페이지를 생성한다.
// pages/posts/[id].js — `pages` 디렉터리
export async function getStaticPaths() {
return {
paths: [],
fallback: 'blocking',
}
}
export async function getStaticProps({ params }) {
// ...
}
export default function Post({ post }) {
return /* ... */
}app 디렉터리에서는 `dynamicParams` 설정이 `generateStaticParams` 밖의 params를 어떻게 처리할지 제어한다.
- `true`(기본값):
generateStaticParams에 포함되지 않은 다이나믹 세그먼트는 필요할 때 생성된다. - `false`:
generateStaticParams에 포함되지 않은 다이나믹 세그먼트는 404를 반환한다.
이는 pages 디렉터리의 getStaticPaths가 가진 fallback: true | false | 'blocking' 옵션을 대체한다. fallback: 'blocking' 옵션은 dynamicParams에 포함되지 않는데, 스트리밍에서는 'blocking'과 true의 차이가 거의 없기 때문이다.
// app/posts/[id]/page.js — `app` 디렉터리
export const dynamicParams = true
export async function generateStaticParams() {
return [
/* ... */
]
}
async function getPost(params) {
/* ... */
}
export default async function Post({ params }) {
const post = await getPost(params)
return /* ... */
}`dynamicParams`를 true(기본값)로 설정하면, 아직 생성되지 않은 라우트 세그먼트가 요청되었을 때 서버에서 렌더링되고 캐시된다.
##### 증분 정적 재생성(getStaticProps의 revalidate)
pages 디렉터리에서는 getStaticProps 함수에 revalidate 필드를 추가해 일정 시간이 지난 뒤 페이지를 자동으로 다시 생성할 수 있다.
// pages/index.js — `pages` 디렉터리
export async function getStaticProps() {
const res = await fetch(`https://.../posts`)
const posts = await res.json()
return {
props: { posts },
revalidate: 60,
}
}
export default function Index({ posts }) {
return (
<Layout>
<PostList posts={posts} />
</Layout>
)
}app 디렉터리에서는 `fetch()`로 데이터를 가져올 때 revalidate를 쓸 수 있다. 지정한 초만큼 요청을 캐시한다.
// app/page.js — `app` 디렉터리
async function getPosts() {
const res = await fetch(`https://.../posts`, { next: { revalidate: 60 } })
const data = await res.json()
return data.posts
}
export default async function PostList() {
const posts = await getPosts()
return posts.map((post) => <div>{post.name}</div>)
}##### API Routes
API Route는 변경 없이 pages/api 디렉터리에서 계속 동작한다. 다만 app 디렉터리에서는 Route Handlers로 대체됐다.
Route Handlers는 Web `Request`와 `Response` API를 사용해 특정 라우트의 커스텀 요청 핸들러를 만들 수 있게 해준다.
export async function GET(request: Request) {}알아두면 좋은 점: 클라이언트에서 외부 API를 호출하려고 API Route를 썼다면, 이제는 Server Component에서 안전하게 데이터를 가져오는 방식을 대신 쓸 수 있다. 자세한 내용은 데이터 fetching을 참고한다.
##### Single-Page Applications
Single-Page Application(SPA)에서 동시에 마이그레이션하는 중이라면 SPA 가이드를 참고한다.
7단계: 스타일링 적용하기#
pages 디렉터리에서는 전역 스타일시트를 pages/_app.js에서만 가져올 수 있었다. app 디렉터리에서는 이 제한이 없어져 전역 스타일을 모든 레이아웃, 페이지, 컴포넌트에 추가할 수 있다.
##### Tailwind CSS
Tailwind CSS를 쓴다면 tailwind.config.js 파일에 app 디렉터리를 추가해야 한다.
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}', // <-- 이 줄을 추가한다
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
}app/layout.js 파일에서 전역 스타일도 import해야 한다.
import '../styles/globals.css'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}Tailwind CSS로 스타일링하기에서 더 알아본다.
두 라우터 함께 쓰기#
서로 다른 Next.js 라우터가 제공하는 라우트 사이를 이동하면 hard navigation이 발생한다. next/link의 자동 prefetch는 라우터를 가로질러 실행되지 않는다.
대신 App Router와 Pages Router 사이의 내비게이션을 최적화해서 prefetch되고 빠른 페이지 전환을 유지할 수 있다.
기능이 deprecated될 때 코드베이스를 갱신하려면 Codemods를 참고한다.
예제 및 데모 설계#
- 데모 가능 여부: 가능
pages와app에 서로 다른 상품 라우트를 두고, Root Layout과 중첩 레이아웃의 적용 범위를 확인한다.getServerSideProps,getStaticProps,getStaticPaths예제를 각각fetch,generateStaticParams,dynamicParams로 바꾼 뒤 응답과 404 동작을 비교한다.next/router와next/navigation훅의 반환값 차이를 화면에서 확인한다.
연습 문제#
pages에서app으로 점진적으로 옮기는 동안 유지해야 하는 파일은 무엇인가?
- A.
pages/_app과pages/_document - B.
pages/404만 - C.
next/head만
정답 보기
정답: A. app/layout.tsx의 스타일은 pages/*에 적용되지 않으므로, 모든 라우트를 옮길 때까지 두 파일을 유지한다.
getStaticPaths의fallback동작을 대체하는 설정은 무엇인가?
- A.
dynamicParams - B.
dynamic - C.
revalidate
정답 보기
정답: A. dynamicParams의 true는 필요할 때 생성하고, false는 404를 반환한다.
챕터 요약#
- App Router는
pages와 함께 둘 수 있으므로 페이지 단위로 옮긴다. - Root Layout, Metadata, 중첩 레이아웃이
_app,_document,next/head,getLayout패턴을 대체한다. - 페이지는 기본적으로 Server Component이며, 이전 중에는 Client Component로 먼저 분리하는 경로가 가장 비슷하다.
fetch,generateStaticParams,dynamicParams, Route Handler가 기존 데이터 함수와 API Route의 역할을 나눈다.- 서로 다른 라우터 사이 이동은 hard navigation이며 자동
prefetch가 라우터를 넘지 않는다.