요즘 pages 디렉토리를 사용한 Next.js 이전 버전의 프로젝트에서 Next.js 13+@ 버전으로 이동하는 작업을 하고 있다. 사실 이전 작업 방법에 대해선 Next.js Docs에 잘써있는데, 그래도 한번 쓱 번역하면서 직접 처음부터 끝까지 글을 한번 쭉 보면 머리에 더 쏙쏙 박히는 것 같아서 한글로 대충 번역을 했다. 근데 이걸 또 공유해달라는 친구가 있어서 정말 러프하게 내가 보려고 대충 쓴 글이지만 블로그에 기록으로 남기려고 한다. 업데이트하는 방법은, 페이지 단위로 아래 글에 적힌 번호 순서대로 쓱쓱 따라가면 된다.
1. npm install next@latest ㄱㄱㄱ
2. src/안에 app 디렉토리를 만든다.
3. Root Layout 폴더를 만든다
- app/layout.tsx
export default function RootLayout({
// Layouts must accept a children prop.
// This will be populated with nested layouts or pages
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
root layout은 기존의 pages/_app.tsx와 pages_document.tsx를 대체하는 친구다.
- _app과 _document 파일이 있다면, 그 친구들의 내용을 (예: 글로벌 스타일 등) root layout으로 옮겨준다.
- app/layout.tsx에 있는 스타일은 pages/에는 적용되지 않는다. 그렇기 때문에, pages/ 라우트가 깨지지 않게 하려면 이동하는 동안에 app과 **document 파일을 유지시켜야 한다! 이동 작업이 끝나면 그때 바이바이하면 된다.
- 만약, 그 두 파일에서 React Context Provider를 사용한다면, 걔내들은 모두 클라이언트 컴포넌트로 이동시켜줘야 한다.
- <head> 테그 관리를 위해선, 빌트인 metadata를 사용한다:
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Home',
description: 'Welcome to Next.js',
}
4. next/head 이동시키기
pages 디렉토리에선, next/head 리엑트 컴포넌트를 사용해서 title이나 meta와 같은 <head> 엘리먼트 관리를 한다.반면, app 디렉토리에선 next/head를 쓰지 않고 자체 지원되는 SEO 기능을 사용한다.
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My Page Title',
}
export default function Page() {
return '...'
}
- 메타 데이터 옵션들: Functions: generateMetadata
5. 페이지들을 하나씩 이동시킨다
pages 디렉토리 안의 페이지들이 디폴트로 클라이언트 컴포넌트인 것과는 다르게, 앱 디렉토리 안에 있는 페이지들은 디폴트로 모두 서버 컴포넌트다. 그렇기 때문에, 앱 디렉토리에선 데이터를 가져오는 방식이 바뀌었다. 기존에 page 디렉토리에서 사용하던 getServerSideProps, getStaticProps, 그리고 getInitialProps는 더 이상 사용되지 않고, 더 간편한 API로 대체되었다!
- app 디랙토리에선 라우트를 정의하기 위해 nested folder 구조가 사용되며, 지정된 파일명 page.tsx를 사용한다.
pages 디렉토리 | app 디렉토리 | 라우트 |
index.js | page.js | / |
about.js | about/page.js | /about |
blog/[slug].js | blog/[slug]/page.js | /blog/post-1 |
페이지들을 이동시킬 땐 아래와 같은 방식을 추천한다.
1. app 디랙토리 아래에 클라이언트 컴포넌트를 export하는 별도의 파일 (app/home-page.tsx)을 추가로 생성한다. 클라 컴포넌트를 사용하기 위해선, 파일 최상단에 ‘use client’를 쓰면 된다
-> 디폴트 exported page 컴포넌트를 pages/index.js에서 app/home-page.tsx로 옮긴다
2. app 디렉토리 안에 새로운 page.tsx를 만든다. 얘는 디폴트로 서버 컴포넌트다.
- 이 page 파일 안에서, home-page.tsx 클라이언트 컴포넌트를 불러온다.
- 만약 pages/index.tsx 파일에서 데이터를 불러오는 작업을 하고 있엇다면, 그 로직을 서버 컴포넌트로 옮긴다. 이때 새로운 데이터 패칭 API를 사용한다
-
-
-
// Import your Client Component 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() { // Fetch data directly in a Server Component const recentPosts = await getPosts() // Forward fetched data to your Client Component return <HomePage recentPosts={recentPosts} /> }
-
-
- 만약, 기존 페이지가 useRouter를 사용한다면, 새로운 next/navigation의 router 친구로 바꿔줘야 한다
- 여기까지 하면, 이 페이지는 app 디렉토리를 통해서 나타나게 된다.
6. Routing 관련 Hook들 사용하기
- app 디렉토리에서의 행동들을 지원하기 위해 새로운 라우터가 추가되었다.
- app에선, next/navigation이 고향인 아래 친구들을 사용해줘야 한다:
- useRouter()
- usePathname()
- useSearchParams()
- 기존의 useRouter는 next/router가 고향인데, 여기 출신 useRouter는 app 디랙토리 내에서 사용할 수 없다.
- 새로운 useRouter
- pathname 스트링을 리턴하지 않는다. 별도로 마련된 usePathname 훅을 써야한다.
- query 객체를 리턴하지 않는다. useSearchParams 훅을 써야한다.
- 기존에 있던 isFallback는 없어졌다. 다른 친구로 대체되었기 때문이다.
- 기존에 있던 locale, locales, defaultLocales, domainLocales 값들도 모두 없어졌다. 빌트인 i18n Next.js기능들이 더이상 필요 없어졌기 때문이다.
- 기존에 있던 basePath도 없어졌다. 얘는 아직 다시 구현되지 않았다.
- 기존에 있던 asPath도 없어졌다. 새로운 라우터에선 as라는 개념 자체를 없애버렸기 때문이다.
- 기존에 있던 isReady도 없어졌다. 더이상 필요하지 않기 때문이다. static rendering 도중엔, useSearchParams() 훅을 사용하는 모든 컴포넌트는 prerendering 단계를 건너뛰고, 대신에 클라이언트의 런타임에 렌더링될 것이다.
- 페이지 변화를 감지하기 위해선, useSearchParams와 usePathname을 사용하면 된다.
- 링크: 라우팅 이벤트
- 이 새로운 훅 친구들은 클라이언트 컴포넌트에서만 지원된다. 서버 컴포넌트에선 사용 못한다.
7. Data fetching 메소드들 이동시키기
- 기존에 pages 디렉토리에선, getServerSideProps와 getStaticProps를 사용해서 페이지들을 위한 데이터를 가져왔다.
- 반면, app 디렉토리에선, 이 두 친구가 더 간단한 API로 대체되었다.
(1) 서버 사이드 랜더링 (getServerSideProps)
- page 디렉토리에선, 서버로부터 데이터를 가져오고 파일에 있는 리액트 컴포넌트한테 props로 넘겨주는 일을 이 친구가 했다. 이 친구를 사용하는 페이지의 최초 HTML은 그렇게 서버에서 프리랜더링되었고, 브라우저에 있는 페이지를 ‘hydrating (= 인터렉션이 가능하게 만드는)’하는 작업이 뒤따랐다.
- 예시)
// `pages` directory
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 디렉토리안에선 서버 컴포넌트라는 개념을 통해 리액트 컴포넌트를 사용해서 데이터 가져오는 걸 한 곳에 모아둘 수 있다. 이렇게 하면, 클라이언트에 더 적은 자바스크립트 코드를 보내는 효과도 있으면서 동시에 서버에서 HTML을 랜더링하는 효과까지 가질 수 있다.
- cache 옵션을 no-store로 설정하면, 가져온 데이터가 절대 캐시로 남지 않도록 설정할 수 있다.
- 예시)
// `app` directory
// This function can be named anything
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>
)
}
- Request object에 접근하기
- pages 디렉토리에선, 요청에 기반한 데이터를 node.js의 HTTP API에 기반하여 가져올 수 있다. 예를들어, req 객체를 getServerSideProps에서부터 가져올 수 있으며, 이걸 요청의 쿠키와 해더를 받는데 사용할 수 있다.
- 예시)
// `pages` directory 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 디렉토리에선, 요청된 데이터를 가져오기 위해 새로운 read-only 함수 headers()와 cookies()를 제공한다. 이 두 친구는 모두 서버 컴포넌트에서 사용된다.
- headers(): Web Headers API에 기반해서 요청된 해더를 가져온다.
- cookies(): Web Cookies API에 기반해서 요청된 쿠키를 가져온다.
- 예시)
// `app` directory import { cookies, headers } from 'next/headers' async function getData() { const authHeader = headers().get('authorization') return '...' } export default async function Page() { // You can use `cookies()` or `headers()` inside Server Components // directly or in your data fetching function const theme = cookies().get('theme') const data = await getData() return '...' }
(2) Static site (getStaticProps)
- pages 디렉토리에선, getStaticProps를 사용해서 빌드 타임에 페이지를 미리 만드는일을 할 수 있었다. 이 친구는 외부 API로부터 데이터를 가져오거나 DB에서 곧장 데이터를 받을 때 사용할 수 있다. 빌드 때 페이지가 만들어지도록 할 수 있는 친구다.
- 예시)
// `pages` directory
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` directory
// This function can be named anything
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>)
}
(3) 다이나믹 패스 (getStaticPaths)
- 이 친구는 page 디렉토리에서 빌드 타임에 미리 랜더링되어야 하는 다이나믹 패스를 만들어주던 아이다.
- 예시)
// `pages` directory
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와 매우 비슷하지만, layout안에서 사용할 수 있는 라우트 파라미터들을 리턴하는 더 간편화된 API를 가지고 있다. 이 친구의 리턴값은 저 위에 pages 디렉토리 예시에 있는 리턴값처럼 복잡하지 않다.
- 예시)
// `app` directory
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/${params.id}`)
const post = await res.json()
return post
}
export default async function Post({ params }) {
const post = await getPost(params)
return <PostLayout post={post} />
}
- API routes
- pages/api에서 그대로 작동한다. 그러나, app 디렉토리 안에 있는 Route Handlers로 대체되었다.
8. 스타일링
- pages 디렉토리에선, 글로벌 stylesheet를 pages/_app.js에만 넣을 수 있었다.
- app 디렉토리에선 이런 제한이 사라졌다. 글로벌 스타일은 이제 모든 layout, page, 컴포넌트 등 어디에든 추가할 수 있다.
- 만약 Tailwind를 사용하다면, app 디렉토리를 tailwind.config 파일에 추가해줘야한당
- 예시)
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}', // <-- Add this line
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
}
- 또한, app/layout.tsx 파일에 글로벌 스타일 파일 가져오는 것도 잊으면 안덴다:
import '../styles/globals.css'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
'Learn to Code' 카테고리의 다른 글
[디자인패턴] 요즘 가장 핫한 프론트엔드 디자인패턴: Feature Sliced Design (번역) (2) | 2024.06.03 |
---|---|
[Next.js] 브라우저 캐싱 문제 해결 방법 (0) | 2023.10.20 |
[React] 커스텀훅(Custom Hook) (0) | 2023.08.16 |
TCP와 UDP (0) | 2023.08.15 |
클라이언트 사이드 렌더링 vs. 서버 사이드 렌더링 (CSR vs. SSR) (0) | 2023.08.03 |
댓글