[Nextjs] Dynamic 페이지를 Static 페이지로 전환하여 풀 라우트 캐시 적용하기

김채운·2024년 12월 9일

Next.js

목록 보기
32/35

Next.js에서 Static 페이지와 Dynamic 페이지를 구분하고, Dynamic 페이지를 Static 페이지로 전환함으로써 풀 라우트 캐시(FRC: Full Route Cache)를 효과적으로 적용하는 방법을 살펴보자.


1️⃣ 문제 상황

쿼리스트링과 빌드 타임의 문제

useSearchParams()와 같은 동적 데이터 관련 훅은 브라우저 환경에서만 동작하며, 빌드 타임에서는 값을 알 수 없기 때문에 에러가 발생할 수 있다.
예를 들어, Searchbar 컴포넌트에서 useSearchParams()를 호출하면, 빌드 타임에는 쿼리스트링(?q=검색어) 값이 존재하지 않기 때문에 오류가 발생하게 된다.

// src/components/searchbar.tsx

"use client";

import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";

export default function Searchbar() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [search, setSearch] = useState("");

  const q = searchParams.get("q");

  useEffect(() => {
    setSearch(q || "");
  }, [q]);

  const onSubmit = () => {
    if (!search || q === search) return;
    router.push(`/search?q=${search}`);
  };

  return (
    <div>
      <input
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && onSubmit()}
      />
      <button onClick={onSubmit}>검색</button>
    </div>
  );
}

2️⃣ 문제 해결: 클라이언트 컴포넌트로 분리

React의 Suspense로 클라이언트 컴포넌트 분리

useSearchParams 훅을 사용하고 있는 Searchbar컴포넌트를 오직 클라이언트 측에서만 실행이 되도록 그래서 사전 렌더링 과정에서는 완전히 배제되도록 설정을 해주면 된다.
그렇게 하기 위해서 React의 Suspense 컴포넌트를 사용해 Searchbar와 같은 클라이언트 컴포넌트를 빌드 타임에서 제외하고, 브라우저 환경에서만 렌더링되도록 설정한다.


// src/app/(with-searchbar)/layout.tsx

import { ReactNode, Suspense } from "react";
import Searchbar from "../../components/searchbar";

export default function Layout({ children }: { children: ReactNode }) {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Searchbar />
      </Suspense>
      {children}
    </div>
  );
}

Suspense의 동작 원리

Suspense로 컴포넌트를 감싸주게 되면 이제부터 이 컴포넌트는 사전 렌더링 과정에서는 배제되고 오직 클라이언트 측에서만 렌더링 되는 컴포넌트로서 설정이 된다.

  • fallback: 비동기 작업이 완료될 때까지 대체 UI를 렌더링.

  • 동작 방식: 서버에서 사전 렌더링 시 Searchbar는 렌더링되지 않고, fallback UI만 렌더링됨.

  • 클라이언트 환경: useSearchParams가 쿼리스트링을 불러오는 이런 컴포넌트의 비동기 작업이 종료가 된 후(컴포넌트가 브라우저에 마운트가 되었을 때)에야 컴포넌트가 완전히 렌더링.


3️⃣ Dynamic 페이지의 원인 분석 및 수정

Next.js에서는 Dynamic 페이지가 캐싱되지 않는 데이터 페칭이나 동적 함수 사용으로 인해 설정된다. 하지만 모든 페이지를 Static 페이지로 만들 수는 없다. 특정 컴포넌트에 대해 캐시 옵션을 통해 Static하게 만들어도 되는 이유를 명확히 이해하고 적용해야 한다.

현재 빌드 결과를 살펴보면, 지금 이 Next 앱에는 index, not-found, book, search페이지가 존재한다. 그리고 각 페이지의 유형은 빌드 결과의 경로 앞에 표시된 기호를 통해 확인할 수 있다.

  • index 페이지: f(function 기호)가 붙어 있어 Dynamic 페이지로 설정되어 있다.

  • not-found 페이지: 빈 동그라미가 붙어 있어 Static 페이지로 설정되어 있다.

  • book, search 페이지: f 기호가 붙어 있어 index 페이지와 마찬가지로 Dynamic 페이지로 설정되어 있다.

결론적으로, 현재 앱에서는 not-found 페이지를 제외한 모든 페이지가 Dynamic 페이지로 설정되어 있다.
이로 인해 풀 라우트 캐시가 전혀 동작하지 않으며, 브라우저의 접속 요청 시 매번 새롭게 렌더링해야 하므로 페이지 응답 속도가 느려질 수 있다.

되도록이면 페이지를 static페이지로 설정해서 풀 라우트 캐시가 적용이 되도록 설정을 하는게 브라우저의 접속 요청을 빠르게 처리할 수 있기 때문에 가능한 컴포넌트인 index 페이지를 static페이지로 전환해 보도록 하자.


✔️ RootLayout 확인

RootLayout은 Index 페이지를 포함해 모든 페이지의 루트 역할을 한다.
이 컴포넌트의 Footer를 확인한 결과, 데이터 페칭 요청에 캐싱 옵션이 누락되어 있었다.

// src/app/layout.tsx

async function Footer() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`);
  const books = await response.json();
  return <footer>{books.length}개의 도서가 등록되어 있습니다.</footer>;
}

수정: force-cache 옵션 적용

force-cache를 추가해 데이터를 강제로 캐싱함으로써 Dynamic 페이지 설정을 방지한다.

async function Footer() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`, {
    cache: "force-cache",
  });
  const books = await response.json();
  return <footer>{books.length}개의 도서가 등록되어 있습니다.</footer>;
}

캐싱 옵션을 적용해도 되는 이유

  • 도서 데이터는 변경 가능성이 없음: 현재 프로젝트에서는 도서를 추가하거나 수정하는 기능이 없으므로, 데이터는 정적으로 유지된다.

  • API 응답의 일관성 보장: /book API는 항상 같은 결과를 반환하며, 데이터가 실시간으로 갱신될 필요가 없다.

  • 성능 최적화: Static 페이지로 설정함으로써 캐시를 활용해 빠르게 페이지를 생성할 수 있다.


✔️ Index 페이지의 컴포넌트 확인

Index 페이지는 AllBooksRecoBooks를 렌더링하고있다.
이 중 AllBooks에서 캐싱 옵션이 누락되어 있어 기본적으로 cache: "no-store"가 적용된다.

// src/app/(with-searchbar)/page.tsx

async function AllBooks() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`);
  const allBooks = await response.json();
  return <div>{allBooks.map((book) => <div key={book.id}>{book.title}</div>)}</div>;
}

수정: force-cache 옵션 추가

force-cache 옵션을 추가해 Static 페이지로 설정한다.

async function AllBooks() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`, {
    cache: "force-cache",
  });
  const allBooks = await response.json();
  return <div>{allBooks.map((book) => <div key={book.id}>{book.title}</div>)}</div>;
}

캐싱 옵션을 적용해도 되는 이유

  • 변경되지 않는 데이터: 프로젝트에서는 도서 목록에 실시간 업데이트가 발생하지 않는다.

  • 일관된 사용자 경험 제공: 모든 요청에서 동일한 데이터를 반환하며, 이를 캐싱해도 사용자 경험에 영향을 미치지 않는다.

  • 성능 최적화: 도서 데이터를 캐싱하여 중복 요청을 방지하고 서버 부하를 줄일 수 있다.


✔️ RecoBooks는 Revalidate 옵션 유지

RecoBooks는 3초마다 데이터를 갱신하도록 설정된 revalidate: 3 옵션을 사용하고 있다.

async function RecoBooks() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/random`, {
    next: { revalidate: 3 },
  });
  const recoBooks = await response.json();
  return <div>{recoBooks.map((book) => <div key={book.id}>{book.title}</div>)}</div>;
}

Revalidate 옵션 유지 이유

  • 랜덤 데이터 제공: /book/random API는 요청마다 새로운 데이터를 반환하며, 데이터를 캐싱하더라도 특정 주기마다 갱신해야 의미가 있다.

  • 동적 콘텐츠의 필요성: 추천 도서는 자주 변경되어야 사용자에게 신선한 경험을 제공할 수 있다.

  • 효율적인 데이터 갱신: 3s 갱신해 데이터의 실시간성을 보장하면서도 성능을 최적화한다.

Dynamic 페이지를 Static 페이지로 전환하는 데 캐시 옵션을 적용할 수 있는 이유는 데이터 변경 가능성과 사용자 경험에 있다.

  • Static 페이지로 적합한 경우: 변경 가능성이 없는 데이터를 캐싱.

  • Dynamic 페이지 유지 필요: 변경 주기가 짧거나 요청마다 다른 결과를 반환해야 하는 경우.

이를 통해 Next.js의 풀 라우트 캐시를 최대한 활용하며, 페이지 로드 속도와 서버 부하를 모두 최적화할 수 있다.


4️⃣ 빌드 후 결과 확인

이제부터는 index페이지가 static페이지로서 잘 설정이 될 것이다. 확인하기 위해서 프로젝트 build를 실행한다.

빌드 결과

  1. Index 페이지는 force-cacheSuspense 설정으로 Static 페이지로 전환되었다. (index페이지 앞에 빈 동그라미 기호가 붙어서 static페이지로서 설정이 된 걸 확인할 수 있다.)

  2. .next/server/app/index.html 파일을 확인하면, Static 페이지로 캐싱된 HTML 파일을 볼 수 있는데, index페이지가 build타임에 미리 생성이 완료되어서 서버 측에 캐싱이 된 것이다. 그리고 이때 이런 기능을 바로 "풀 라우트 캐시"라고 부른다.

브라우저 테스트

  1. npm run start로 서버를 실행하고 브라우저에서 Index 페이지를 새로고침한다.

  2. 캐싱된 html페이지(static페이지)가 풀 라우트 캐시에서 빠르게 제공되기 때문에 굉장히 빠른 속도로 화면이 나타나는 것을 확인할 수 있다.

  3. RecoBooks 컴포넌트 덕분에 3s 주기로 최신 데이터가 반영된다.


5️⃣ 결론

Dynamic → Static 전환 요약

  1. force-cache를 통해 데이터 페칭 결과를 캐싱.

  2. Suspense로 클라이언트 컴포넌트를 빌드 타임에 서버 렌더링에서 제외.

  3. Revalidate 옵션을 사용해 정적 페이지도 특정 주기로 갱신 가능.

풀 라우트 캐시의 이점

  1. 정적 HTML 파일을 빌드 타임에 생성하여 빠른 응답 속도 제공.

  2. Revalidate 옵션과 결합해 동적 데이터도 효과적으로 관리 가능.

이처럼 Dynamic 페이지를 Static 페이지로 전환해 Next.js의 성능 최적화를 적용할 수 있다.
Dynamic한 데이터가 필요하지 않은 페이지는 가능한 Static 페이지로 설정하는 것이 성능과 사용자 경험에 유리하다.

0개의 댓글