NextJS 앱 라우터 마이그레이션

Google Search Console에 문제가 발생해서 metadata를 손쉽게 설정할 수 있는 NextJS 앱 라우터로 마이그레이션을 하게 되었다.
다시 동일한 작업을 한다고 가정했을 때 유념해야 할 점들을 기록해 둔다.

기본 폴더 구조 🔗

기존의 /pages가 아닌 /app에 각 url의 페이지를 두는 방식이다. index.tsxpage.tsx로 대체되고 보통 Layout 컴포넌트로 페이지를 감싸는 것처럼 같은 위치에 layout.tsx를 둘 수 있다.
layout.tsx에서는 page.tsx를 감싸는 provider를 넣거나 Header 컴포넌트 등을 넣을 수 있다.
자식 라우트에 layout.tsx를 추가하더라도 조상과 중첩되서 적용된다.
때문에 최상단 layout.tsx에 페이지 라우터의 _app.tsx_document.tsx의 내용을 옮겨 넣으면 된다.

https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required

이렇게 pagelayout이라는 예약어가 정해져있기 때문에 예를 들어 constants.ts 등과 같은 파일들을 따로 빼지 않고 함께 넣을 수 있게 되었다.

https://nextjs.org/docs/app/building-your-application/routing

Root Layout 🔗

모든 라우트에 골격이 되는 html이나 body 태그가 적용되도록 최상단 layout.tsx를 다음과 같이 설정해야 한다.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

서버 컴포넌트와 클라이언트 컴포넌트 🔗

/pages는 클라이언트 컴포넌트였지만 /app에 있는 페이지들은 기본적으로 서버 컴포넌트다.
여전히 getServerSideProps와 같이 서버 코드가 돌아가야할 부분과 클라이언트 코드가 돌아가야할 부분이 구분돼야 하기 때문에 마이그레이션시 아래와 같이 나누는 것을 권장한다.

  • /app/page.tsx
    • 서버 컴포넌트
  • /app/home-page.tsx
    • 클라이언트 컴포넌트
    • 'use client'를 제일 위에 명시

파일의 첫 번째 줄에 'use client'라고 명시하면 client 컴포넌트가 된다.
context provider의 경우 client 컴포넌트에 위치해야 한다.

https://nextjs.org/docs/app/building-your-application/rendering/client-components

(🚧 TODO: 리액트 서버 컴포넌트와 클라이언트 컴포넌트에 대해서)

getS**Props 🔗

NextJS를 처음 접하면 다소 괴상해 보일 수 있는 getStaticProps, getServerSideProps, getStaticPaths가 사라지고 단순하게 처리할 수 있게 되었다.

getServerSideProps 🔗

React의 서버 컴포넌트를 이용하게 되면서 컴포넌트 내부에서 데이터를 불러올 수 있게 되었다.
cache 옵션을 no-store로 설정하면 getServerSideProps처럼 요청할 때마다 새로운 데이터가 적용된 페이지를 얻을 수 있다.
아래와 같이 getServerSideProps를 대신해서 사용한다.

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

getStaticProps 🔗

이와 반대로 getStaticProps처럼 빌드 시점에서 미리 페이지를 찍어내려면 cache 옵션에 아무것도 넣지 않는다.
기본값이 force-cache다.

getStaticPaths 🔗

getStaticPathsgenerateStaticParams로 대체됐다.
layout.tsx에서도 사용할 수 있게 되었다.
getStaticPaths에서 fallback 옵션을 활성화하면 빌드 시점에 페이지를 생성해 두지 않고 라이브에서 요청을 받았을 때 각각 생성할 수 있었다.
그러나 이제는 fallback 옵션이 기본으로 설정되고 아래에서 바꿀 수 있다.

https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams

fetch 🔗

browser의 fetch api를 확장시킨 함수로 사용시 별다른 옵션을 넣지 않으면 cache가 적용된다.
ISR을 위해 아래처럼 revalidate 옵션을 넣을 수 있다.

async function getPosts() {
  const res = await fetch(`https://.../posts`, { next: { revalidate: 60 } });
  const data = await res.json();

  return data.posts;
}

https://nextjs.org/docs/app/api-reference/functions/fetch

cache 🔗

fetch를 사용하는 경우 자동으로 memoized 되지만 graphql 등 fetch를 사용할 수 없는 경우 React.cache를 이용한다.

https://nextjs.org/docs/app/building-your-application/caching#react-cache-function

import { cache } from 'react';
import db from '@/lib/db';

export const getItem = cache(async (id: string) => {
  const item = await db.item.findUnique({ id });
  return item;
});

metadata 🔗

next/head를 이용한 방법 대신 각 페이지마다 데이터를 받아서 metadata를 설정할 수 있게 되었다.
next-seo 같은 라이브러리를 안 써도 된다.

https://nextjs.org/docs/app/api-reference/functions/generate-metadata

https://nextjs.org/docs/app/building-your-application/optimizing/metadata

useRouter 🔗

useRouter의 기능이 쪼개졌다.
pathname을 알고 싶으면 usePathname을, query를 알고 싶으면 useSearchParams를 쓰자.

'use client';

import { useRouter, usePathname, useSearchParams } from 'next/navigation';

export default function ExampleClientComponent() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const page = searchParams.get('page');

  // ...
}

Search Params 변경 방법 🔗

URLSearchParams을 이용해서 아래처럼 params.set이나 params.delete 등으로 search params를 업데이트 할 수 있다.

export default function ExampleClientComponent() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const handlePageChange = (page: number) => {
    const params = new URLSearchParams(searchParams.toString());
    params.set('page', page);
    params.delete('tag');
    const newParams = params.toString();
    router.push(pathname + '?' + newParams);
  };

  // ...
}

Entire page deopted into client-side rendering 에러 🔗

useSearchParams를 잘못 쓰면 생기는 에러로 빌드할 때 터미널에서 warning으로 보여준다.

https://nextjs.org/docs/messages/deopted-into-client-rendering

useSearchParams를 쓰는 컴포넌트를 아래처럼 <Suspense>로 감싸줘야 한다.

<Suspense fallback={<Fallback />}>
  <ComponentWithSearchParams />
</Suspense>

페이지의 모든 내용이 client side에서 렌더링 되면 SEO에 좋지 않기 때문에 useSearchParams를 쓸 때 유의해야 한다.