본문 바로가기
  • 프론트엔드 개발자 세오세오 | Frontend dev Seo
Learn to Code

[Next.js] 버전 13+으로 업데이트하기

by CEOSEO 2023. 10. 7.
728x90
반응형

요즘 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 | Next.js

This page covers all Config-based Metadata options with generateMetadata and the static metadata object. To define static metadata, export a Metadata object from a layout.js or page.js file. Dynamic metadata depends on dynamic information, such as the curr

nextjs.org

 

 

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를 사용한다

      1. // 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을 사용하면 된다.
  • 이 새로운 훅 친구들은 클라이언트 컴포넌트에서만 지원된다. 서버 컴포넌트에선 사용 못한다.
 

Functions: useRouter | Next.js

Using App Router Features available in /app

nextjs.org

 

 

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>
  )
}

 

 

728x90
반응형

댓글