Next.js 시작하기: 3편에서
pages
사용 방법에 대해 알아보았다. 이번엔app
과page
의 차이에 대해 알아보자.
Next.js 13.4부터는 App 라우터를 안정성 있게 사용할 수 있다.
app/layout.js
/*
The root layout for the entire application
*/
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
);
}
Pages 라우터에서 컴포넌트를 한 겹 감싸기 위해서는 Layout
컴포넌트를 만들고 해당 컴포넌트를 불러와서 사용해야 했다. 하지만 App 라우터는 Layout
컴포넌트를 가져오지 않아도 자동으로 감싸진다. 레이아웃은 중첩하여 사용할 수 있다.
app/page.js
export default function Page() {
return (
<h1>Hello, Next.js!</h1>
);
}
💥app 경로 vs. page 경로
pages 디렉토리 | app 디렉토리 | 경로 |
---|---|---|
index.js | page.js | / |
about.js | about/page.js | about |
blog/[id].js | blog/[id]/page.js | /blog/1 |
Next.js 파일은 중첩된 경로에서 특정 동작으로 UI를 생성하기 위해 특수한 파일들을 사용한다.
layout
: Shared UI for a segment and its childrenpage
: Unique UI of a route and make routes publicly accessibleloading
: Loading UI for a segment and its childrennot-found
: Not found UI for a segment and its childrenerror
: Error UI for a segment and its childrenglobal-error
: Global Error UIroute
: Server-side API endpointtemplate
: Specialized re-rendered Layout UIdefault
: Fallback UI for Parallel RoutesApp 라우터는 각 경로의 segment에 대한 UI를 생성하기 위해 특수 파일 규칙을 사용한다. page
는 경로에 고유한 UI를 표시하고, layout
은 하위 경로에서 공유되는 UI를 표시한다.
Next.js는 폴더를 사용하여 경로를 정의하는 파일 시스템 기반 라우터를 사용한다. 각 폴더는 URL segment에 매핑된다. 중첩된 경로를 만들려면 폴더를 중첩하면 된다. 특수한 page.js
파일을 사용하면 경로 segment에 공개적으로 접근할 수 있다.
위의 예시에서 /dashboard/analytics
경로는 page.js
파일이 없으므로 접근할 수 없다. 반면, /dashboard
는 page.js
를 하위 파일로 갖고 있기 때문에 접근 가능하다.
pages
to app
<Image />
컴포넌트 (next/image
)<Link />
컴포넌트 (next/link
)<Script />
컴포넌트 (next/script
)page
to app
app
디렉토리는 중첩된 경로와 레이아웃을 지원한다.page.js
: define UI unique to a routelayout.js
: define UI that is shared across multiple routesgetServerSideProps
와 getStaticProps
같은 기존 데이터 fetching 함수들은 app
의 새로운 api로 대체된다.pages/_app.js
와 pages/_document.js
는 app/layout.js
의 RootLayout
으로 대체된다.pages/_error.js
는 error.js
, pages/404.js
는 not-found.js
로 대체된다./*
app/layout.js
app 내부의 모든 경로에 적용되는 루트 레이아웃
*/
export default function RootLayout({ chidren }) {
return (
<html>
<body>{children}</body>
</html>
);
}
app
디렉토리는 RootLayout
을 포함해야 한다.RootLayout
은 <html>
과 <body>
태그를 정의해야 한다.next/head
pages
디렉토리에서는 next/head
의 리액트 컴포넌트를 사용하여 <title>
이나 <meta>
같은 HTML 요소들을 포함하는 <head>
를 관리했다. app
디렉토리에서는 built-in SEO로 대체되어 사용된다.
import { Metadata } from 'next';
export const metadata = {
title: 'My Page Title'
}
export default function Page() {
return ...
}
템플릿은 하위 레이아웃이나 페이지를 감싸는 레이아웃과 유사하다. 하지만 경로 전반에 걸쳐 지속되고 상태롤 유지하는 레이아웃과 달리, 템플릿은 각 하위 항목에 대해 새로운 이스턴스를 만든다. 즉, 템플릿을 공유하는 경로 사이를 왔다갔다할 때, 컴포넌트의 새 인스턴스가 마운트 되고, DOM 요소가 다시 생성되며, 상태가 유지되지 않고 효과가 다시 동기화 된다. 템플릿은 다음 사례에서 유용하게 쓰인다.
useEffect
와 useState
에 의존할 때export default function Template({ children }) {
return <div>{children}</div>
}
서버 및 클라이언트 컴포넌트를 사용하면 클라이턴트 측의 풍부한 상호작용과 기존 서버 렌더링의 향상된 성능을 결합하여 서버와 클라이언트를 포괄하는 어플리케이션을 만들 수 있다.
서버 컴포넌트는 서버와 클라이언트를 아우르는 하이브리드 어플리케이션을 만들기 위한 모델이다. SPA처럼 리액트가 전체 어플리케이션을 클라이언트 측에서 렌더링 하는 대신, 목적에 따라 렌더링 위치를 선택할 수 있는 유연성을 제공한다.
위와 같은 페이지가 있다고 가정해보자. 페이지를 작은 컴포넌트들로 나누었을 때, 대다수의 컴포넌트들이 상호작용을 필요로 하지 않고 서버 컴포넌트로 렌더링이 가능하다는 것을 알 수 있다. 서버 컴포넌트를 이용하면 클라이언트 컴포넌트에 비해 어떤 이점이 있을까?
서버 컴포넌트를 이용하면 서버 인프라를 효과적으로 활용할 수 있다. 예를 들어, 이전에 클라이언트 자바스크립트 번들 크기에 영향을 미쳤던 large dependencies가 서버에 대신 남기 대문에 성능을 향상 시킬 수 있다. 이처럼 서버 컴포넌트는 리액트 어플리케이션을 PHP, Ruby on Rails와 유사하게 느끼도록 하지만 리액트의 강력함과 유연성도 사용할 수 있다.
또한 서버 컴포넌트를 사용하면 초기 페이지 로드가 빨라지며 클라이언트 측의 자바스크립트 번들 크기가 줄어든다.
클라이언트 구성 요소를 사용하면 어플리케이션에 상호작용을 추가할 수 있다. 클라이언트 컴포넌트는 페이지 라우터 방식으로 볼 수 있다.
use client
💥 언제 서버/클라이언트 컴포넌트를 사용해야 할까
클라어언트 컴포넌트 사용 사례가 있을 때까지 서버 컴포넌트를 사용하는 게 좋다.
클라이언트 컴포넌트 사용 사례
- 상호작용 및 이벤트 :
onClick()
,onChange()
등- 상태 및 수명 주기 사용 :
useState()
,useReducer()
,useEffect()
등- 브라우저 전용 API 사용
Next.js에서 경로를 탐색하는 방법은 2가지다.
1. <Link>
컴포넌트 사용하기
2. useRouter
훅 사용하기
<Link>
Component<Link>
는 <a>
HTML 태그를 확장하여 경로 간 prefetch와 클라이언트 측 탐색을 제공하는 내장 컴포넌트다. next/link
에서 가져와 사용하고, href
값을 전달한다.
/*
Linking to dynamic routes
*/
import Link from 'next/link'
export default function PostList({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/posts/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}
💥 스크롤 위치 조작하기
Next.js의 App 라우터는 기본적으로 새 경로는 스크롤을 맨 위로 올리고, 앞뒤 탐색은 스크롤 위치를 유지한다. 특정 항목으로 스크롤하려면 url에 #
을 추가하여 항목의 위치로 이동할 수 있다.
<Link href='/dashboard#settings'>Settings</Link>
Next.js의 기본 동작을 비활성화 하고 싶다면 scroll={flase}
를 전달하면 된다.
useRouter
를 사용하는 경우import { useRouter } from 'next/navigation'; router.push('/dashboard', {scroll: false});
useRouter
HookuseRouter
를 사용하면 프로그래밍 방식으로 경로를 변경할 수 있다. useRouter
는 클라이언트 컴포넌트 내부에서 사용되며 next/navigation
에서 가져와야 한다.
'use client'
import { useRouter } from 'next/navigation';
export default function Page() {
const router = useRouter();
return (
<button type='button' onClick={() => router.push('/dashboard')}>
Dashboard
</button>
);
}
App 라우터는 하이브리드 접근 방식을 사용한다. 서버에서 어플리케이션 코드는 경로 segment별로 자동으로 코드 분할이 이루어진다. 그리고 클라이언트에서는 경로 segment를 prefetch하고 캐시한다.
사용자가 새 경로로 이동하면 브라우저는 페이지를 다시 로드하지 않고 변경된 경로 segment만 다시 렌더링하여 탐색 경험과 성능을 향상시킨다.
사용자가 방문하기 전에 백그라운드에서 경로를 미리 불러오는 것을 의미한다. Next.js에서 경로를 prefetch하는 방법은 두 가지다.
<Link>
컴포넌트 (경로가 사용자의 뷰포트에 표시되면 자동으로 prefetch)useRouter
의 router.prefetch()
Next.js는 라우터 캐시라 불리는 클라이언트 측 메모리 캐시가 존재한다. 사용자가 어플리케이션을 탐색할 때, 미리 가져온 경로 segment와 방문한 경로들이 저장된다. 이는 탐색 시 서버에 새로운 요청을 보내는 대신 캐시를 최대한 재사용하여 성능을 향상시킨다는 의미다.
부분 렌더링은 바뀐 경로의 segment만 렌더링하는 것을 의미한다. 예를 들어 /dashboard/settings
와 /dashboard/analytics
사이를 탐색한다고 가정하자. settings
와 analytics
는 렌더링되지만 서로 공유하는 dashboard
레이아웃은 보존된다.
브라우저는 기본적으로 페이지를 다시 로드하고 어플리케이션의 useState
같은 상태를 초기화하며 사용자의 스크롤 위치 같은 브라우저 상태도 초기화한다. 하지만 Next.js의 App 라우터는 부드러운 탐색을 지원한다. 바뀐 부분만 렌더링하고 리액트와 브라우저 상태를 유지한다.
Next.js는 앞으로/뒤로 가기를 수행해도 스크롤의 위치를 유지하고 라우터 캐시의 경로 segment를 재사용한다.
app
디렉토리의 하위 경로는 url 경로로 매핑된다. 하지만 특정 디렉토리명을 url에서 생략하고 싶은 경우가 생길 수 있다. 이때 경로 그룹을 사용한다. (folderName)
경로 그룹은 특정 세그먼트를 레이아웃으로 선택할 수 있다. shop
의 하위 페이지인 account
와 cart
는 레이아웃을 공유해야 한다고 가정하자. shop
을 경로 그룹으로 변경하고 (shop)
, account
와 cart
를 하위 디렉토리로 생성하고 그 밑에 page.js
를 만든다. account
와 cart
는 파일 컨벤션에 의해 상위 레이아웃이 적용된다.
generateStaticParams
generateStaticParams
는 동적 경로와 SSG를 함께 사용하고 싶을 때 사용한다.
Pages 라우터에서는 getStaticPaths
와 getStaticProps
를 통해 SSG를 구현했다. generateStaticParams
는 Pages 라우터의 getStaticPaths
를 대체하는 함수다.
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then(res => res.json());
return posts.map(post => ({
id: post.id
}));
}
export default function Post({ params }) { ... }
경로 | generateStaticParams 반환 타입 |
---|---|
/product/[id] | { id: string }[] |
/products/[category]/[product] | { category: string, product: string }[] |
경로 핸들러는 웹 요청과 응답 api를 이용하여 사용자 경로를 만들 수 있다. 경로 핸들러는 app
디렉토리 내의 route.js|ts
파일에 정의된다. 경로 핸들러는 app
디렉토리에서 중첩될 수 있다.
경로 핸들러는 오직
app
디렉토리 내에서만 사용 가능하다.
/*
app/api/route.js
*/
import { NextResponse } from 'next/server';
export async function GET() {
const res = await fetch('https://...', {
headers: {
'Content-Type': 'application/json',
}
});
const data = await res.json();
return NextResponse.json({data});
}
import { NextResponse } from 'next/server';
json()
: 주어진 JSON body로 응답 생성export async function GET(request) {
return NextResponse.json({ error: 'Internal Server Error'}, {status: 500});
}
redirect()
const loginUrl = new URL('/login', request.url);
return NextReponse.redirect(loginURL);
cookies
response.cookies.set('show-banner', 'false');
참고자료
NEXT.js : app router migration
NEXT.js : defining routes
NEXT.js : route handlers
NEXT.js : nextResponse