[Nextjs] 동적 페이지 Static 페이지로 전환 방식

김채운·2024년 12월 12일

Next.js

목록 보기
33/35

이번 포스트에서는 Next.js 앱의 Static 페이지 설정 과정을 자세히 살펴보도록 하자. 지난 포스트에서 index 페이지를 Static 페이지로 변환했듯이, 이번에는 search 페이지와 book같이 동적인 페이지를 대상으로 Static 페이지로 전환 가능한지 분석하고, 필요한 설정 및 최적화 방법을 살펴보자.


1️⃣ search 페이지 분석 및 최적화

search 페이지는 사용자가 입력한 검색어를 기반으로 백엔드에서 데이터를 가져와 화면에 표시한다. 이를 구현한 코드의 주요 내용은 아래와 같다.

import BookItem from "@/components/book-item";
import { BookData } from "@/types";

export default async function Page({
  searchParams,
}: {
  searchParams: {
    q?: string;
  };
}) {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/search?q=${searchParams.q}`, {
    cache: "force-cache",
  });
  if (!response.ok) {
    return <div>오류가 발생했습니다...</div>;
  }

  const books: BookData[] = await response.json();

  return (
    <div>
      {books.map((book) => (
        <BookItem key={book.id} {...book} />
      ))}
    </div>
  );
}

🔍 문제점: Static 페이지 전환 불가

이 페이지는 searchParams라는 쿼리스트링 보관하는 props를 받아 검색 결과를 가져오는 동적 함수를 사용하기 때문에 Dynamic페이지로서 존재하는데 이 페이지는 Static 페이지로 설정할 수 없다. 이유는,
Static 페이지는 빌드 타임에 완전히 렌더링되지만, 쿼리스트링은 런타임에 브라우저에서 전달되므로 빌드 타임에는 존재하지 않는 값이기 때문이다.

✅ 해결 방안: 데이터 캐시 활용

Static 페이지로 전환할 수 없어서 풀 라우트 캐시는 포기해야 하지만, 데이터를 효율적으로 관리하기 위해 데이터 캐시를 활용하여 검색 속도를 개선할 수 있다. fetch 메서드의 cache: "force-cache" 옵션을 설정하면, 브라우저로부터 접속 요청을 받았을 때 페이지는 계속해서 다시 생성이 되겠지만 이미 검색된 데이터는 캐시에서 빠르게 반환되어 응답 속도가 빨라진다.

정리하자면, search페이지는 index페이지와는 다르게 쿼리스트링 같은 동적인 값에 의존하고 있다. 그렇기 때문에 static페이지로는 설정할 수 없지만 데이터 캐시를 활용하는 쪽으로 최적화를 해줄 수 있다.


2️⃣ book 페이지 분석 및 Static 설정

book 페이지는 URL에 포함된 id를 기반으로 특정 도서 정보를 렌더링한다. 코드는 다음과 같다.


// src/app/book/[id]/page.tsx

import { notFound } from "next/navigation";
import style from "./page.module.css";

export function generateStaticParams() {
  return [{ id: "1" }, { id: "2" }, { id: "3" }];
}

export default async function Page({
  params,
}: {
  params: { id: string | string[] };
}) {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/${params.id}`);
  if (!response.ok) {
    return <div>오류가 발생했습니다...</div>;
  }

  const book = await response.json();
  return (
    <div className={style.container}>
      <div
        className={style.cover_img_container}
        style={{ backgroundImage: `url('${book.coverImgUrl}')` }}
      >
        <img src={book.coverImgUrl} />
      </div>
      <div className={style.title}>{book.title}</div>
      <div className={style.author}>{book.author} | {book.publisher}</div>
      <div className={style.description}>{book.description}</div>
    </div>
  );
}

🔍 문제점: 기본적으로 Dynamic 페이지

이 페이지는 지금 id라는 URL 파라미터를 갖는 여러개의 동적 경로에 대응하는 페이지이다. 하지만 현재 코드에서는 URL에 동적으로 전달되는 id 값이 무엇인지 알 수 없다.따라서 Next.js는 이 페이지를 Dynamic 페이지로 설정한다.
Dynamic 페이지는 요청이 들어올 때마다 실시간으로 서버에서 데이터를 가져와 페이지를 생성하므로, 정적으로 캐싱된 페이지에 비해 응답 속도가 느리다.

✅ 해결 방안: generateStaticParams 함수 활용

generateStaticParams는 빌드 타임에 생성될 URL 파라미터를 명시하여 Static 페이지로 설정하는 함수로, 빌드 타임에 Next서버가 book페이지에 어떠한 경로들이 올 수 있는지 그래서 어떠한 URL 파라미터들이 존재할 수 있는지 알려준다. 쉽게 말하면 이 book페이지에 어떤 도서 데이터들이 빌드 타임에 만들어져야 하는지 먼저 알려주는 것이다. 아래와 같이 설정하면, id가 1, 2, 3인 도서의 페이지를 빌드 타임에 미리 생성할 수 있다.

export function generateStaticParams() {
  return [{ id: "1" }, { id: "2" }, { id: "3" }];
}

⚙️ 동작 방식

  1. Next.js는 빌드 타임에 generateStaticParams에서 반환된 URL 파라미터(id: 1, 2, 3)를 읽는다.

  2. 각 파라미터 값에 해당하는 페이지를 미리 렌더링하여 Static 페이지로 생성한다. (ex: /book/1,/book/2,/book/3)

  3. 생성된 페이지는 풀 라우트 캐시에 저장된다.

  4. 정적으로 설정되지 않은 /book/4 같은 경로는 Dynamic 페이지로 동작하며, 요청 시 실시간으로 생성된다.

실제로 프로젝트를 가동 중단한 다음에 다시 빌드를 해보면

빌드 결과 1, 2, 3 페이지가 static페이지로서 빌드타임에 미리 생성이 된 걸 볼 수 있다. 그리고 또 그렇기 때문에 마찬가지로 .next폴더 안에 server폴더 안에 app폴더 안에 book폴더를 보면

book/1.html, book/2.html, book/3.html 페이지가 빌드 타임에 렌더링이 완료가 되어서 서버 측에 풀 라우트 캐시로서 잘 보관이 된 걸 볼 수 있다.

🔄 실시간 Dynamic 페이지의 동작

만약 /book/4 경로로 접속하면 어떻게 될까?

  • /book/4generateStaticParams에 포함되지 않았으므로, 빌드 타임에 정적으로 생성되지 않는다.

  • 따라서 Dynamic 페이지로 동작하여, 서버에서 데이터를 가져온 후 실시간으로 페이지를 생성한다.

  • 생성된 페이지는 풀 라우트 캐시에 저장된다.

🛠 Dynamic 페이지의 실시간 생성

  1. 처음 /book/4에 접속하면 실시간 데이터 페칭을 통해 페이지를 생성한다.

  2. 따라서 Dynamic 페이지로 동작하여, 서버에서 데이터를 가져온 후 실시간으로 페이지를 생성한다.

  3. 다음 요청부터는 캐시에 저장된 페이지를 반환하므로 더 빠른 응답 속도로 제공한다. (풀 라우트 캐시에 저장)

📊 속도 비교

실제로 속도 변화가 있는지 확인하기 위해서 아직 생성되지 않은 페이지인 /book/6으로 접속해 보면

  1. /book/6에 첫 요청 시 페이지 생성과 데이터 페칭 작업이 함께 이루어지므로 약 65ms가 소요된다.

  1. 이후 두 번째 요청부터는 풀 라우트 캐시에서 반환되므로 약 9ms로 대폭 줄어들었다.

❗주의할 점

  1. generateStaticParams에서 URL 파라미터 값을 명시할 때에는 문자열 데이터로만 명시해 줘야 한다.

  2. generateStaticParams 함수를 내보내주게(export)되면 페이지 컴포넌트 내부에 데이터 캐싱이 설정되지 않은 페칭이 존재할지라도 무조건 해당하는 페이지는 static페이지로 강제 설정이 된다.

🔔 추가 최적화: 404 처리

존재하지 않는 id를 요청하면 오류 메시지를 표시하기보다 404 페이지로 리다이렉트하는 것이 더 적합하다. 이를 위해 notFound 함수를 사용해 보자.


// src/app/book/[id]/page.tsx

  if (!response.ok) {
    if (response.status === 404) {
      notFound();
    }
    return <div>오류가 발생했습니다...</div>
  }

페이지 컴포넌트에서 데이터를 불러왔을 때 response.ok가 false일 때 이 안에 조건문을 새롭게 추가해서 이 조건문에서는 response의 현재 status의 코드가 404 즉 notFound코드가 백엔드 서버로부터 돌아왔다면 이런 경우에는 도서의 데이터가 현재 없다는 거니까 notFound라는 함수를 next/navigation으로부터 불러와서 호출을 해준다. 그럼 자동으로 404페이지로 페이지가 redirect된다.

404페이지는 이렇게 작성해 준다.

// src/app/not-found.tsx

export default function NotFound() {
    return <div>404: NotFound</div>
}

이렇게 notFound페이지를 컴포넌트로 만들어준다.

실제로 잘 작동하는지 확인하기 위해서 프로젝트를 빌드한 다음에 가동시켜준다. 그리고 존재하지 않는 id로 접속을 해보면,

이렇게 NotFound페이지가 잘 redirect 되는 걸 볼 수 있다.


5️⃣ Dynamic 페이지 제한

generateStaticParams에서 정적으로 설정한 경로 외의 URL을 허용하지 않으려면 dynamicParams 값을 false로 설정한다.

 export const dynamicParams = false;

그럼 Next 서버가 이 페이지를 생성할 때 자동으로dynamicParams 변수의 값을 확인해서 false로 내보내졌으니까 지금 이 페이지의 URL 파라미터는 다이나믹 하면 안 되겠구나 하고 generateStaticParams로 부터 내보내진 /book/1, /book/2, /book/3 이외의 요청에는 모두 404 페이지로 리다이렉트된다.


6️⃣ 결론

📋 Static 경로

  • /book/1, /book/2, /book/3: 빌드 타임에 생성된 정적 페이지.

  • 빠른 응답 속도 제공.

📋 Dynamic 경로

  • /book/4, /book/5: 실시간으로 생성 후 캐시에 저장.

  • 이후 요청 시 캐시에서 반환되어 빠른 속도 제공.

📋 404 처리

  • 존재하지 않는 id는 notFound함수 이용해 404 페이지로 리다이렉트.

📋 Dynamic 제한

  • dynamicParams = false를 설정하여 허용되지 않은 URL은 404 페이지로 리다이렉트 가능.

📋 동적 경로와 Static 페이지 최적화의 조화

  • 빌드 타임에 자주 사용되는 경로를 미리 설정하고, 예외적으로 동작해야 하는 경로는 Dynamic으로 처리하며 적절히 최적화.

0개의 댓글