전체 글 보기

서버 컴포넌트 렌더링 전략

profile icon

서버 컴포넌트의 렌더링 전략을 이해하고, 주의사항을 살펴본다. 또한 Parallel Routes와 함께 사용할 때 발생할 수 있는 이슈와 그 해결 방법에 대해서도 다루어본다.

#Next.js
마지막 수정일:

리액트의 전통적인 컴포넌트인 클라이언트 컴포넌트는 본인이 가지고 있는 상태(state), 혹은 부모의 상태가 변화하면 리렌더링이 일어난다. 하지만 서버 컴포넌트에서는 hook을 쓸 수 없다. 즉, 서버 컴포넌트는 상태를 가지지 않는다는 뜻이다.

아래 예시 처럼 부모의 상태가 바뀌어도 서버 컴포넌트는 리렌더링 되지 않는다. 그렇다면 서버 컴포넌트는 언제 리렌더링 되는걸까?

server-component-rendering-strategies-example1

Next.js 공식 문서에 따르면 Static, Dynamic, Streaming 3가지 전략으로 렌더링을 진행한다.

Static Rendering은 Next.js의 기본 렌더링 전략이다. 이 전략의 핵심 특징은 빌드 타임에 컴포넌트를 한 번 렌더링하고, 그 결과를 재사용한다는 점이다.

  1. 화면을 새로고침 했을때

    server-component-rendering-strategies-example2

    state를 바꾸고 reload해도 데이터가 바뀌지 않는 것을 확인할 수 있다.

  2. revalidate를 했을 때

    server-component-rendering-strategies-example3

    revalidateTag를 실행하면 서버 컴포넌트의 캐시가 무효화되고, 데이터를 다시 불러와 리렌더링하는 것을 확인할 수 있다.

Dynamic Rendering은 사용자 요청마다 서버에서 새롭게 컴포넌트를 렌더링하는 전략이다.

서버 컴포넌트는 다음 두 가지 조건 중 하나라도 만족하면 자동으로 Dynamic Rendering으로 전환된다.

  1. Dynamic API 사용
  2. fetch 옵션에 { cache: 'no-store' } 적용
Dynamic API 사용데이터 캐싱렌더링 결과
❌ No✅ CachedStatic
✅ Yes✅ CachedDynamic
❌ No❌ Not CachedDynamic
✅ Yes❌ Not CachedDynamic

즉, Dynamic API를 사용하지 않으면서 fetch에 캐싱을 적용해야만 Static Rendering이 된다.

Dynamic API는 요청 시점의 컨텍스트에 의존하는 API다. 주요 Dynamic API는 다음과 같다.

tsx
import { cookies } from 'next/headers';

async function Component() {
  const cookieStore = cookies(); 
  const theme = cookieStore.get('theme');
  return <div>Current theme: {theme?.value}</div>;
}
tsx
import { headers } from 'next/headers';

async function Component() {
  const headersList = headers(); 
  const userAgent = headersList.get('user-agent');
  return <div>Accessing from: {userAgent}</div>;
}
tsx
export default function Page({
  searchParams,
}: {
  searchParams: { query: string };
}) {
  return <div>Search query: {searchParams.query}</div>;
}

아래 예제처럼 searchParams로 모달을 제어할 때, Dynamic Rendering으로 인해 다음 문제가 발생할 수 있다.

tsx
'use client'

import {useSearchParams} from "next/navigation";
import {Modal} from "@/app/@modal/(.)post/[id]/modal";

function Default() {
  const searchParam = useSearchParams();
  const isModalOpen = searchParam.get('modal') === 'true';

  if (!isModalOpen) {
    return null;
  }

  return (
    <Modal>modal!!</Modal>
  );
}

export default Default;

searchParam이 붙으면서 모달이 열릴 때, 기존 컴포넌트가 리렌더링 되는걸 볼 수 있다.

server-component-rendering-strategies-example4

가장 간단하고 효과적인 방법은 Next.js의 라우팅 시스템을 우회하여 브라우저의 내장 History API를 직접 사용하는 것이다.

tsx
'use client';

function ProductList() {
  const openModal = () => {
    // Next.js 라우팅을 거치지 않고 URL 직접 변경
    history.pushState(null, '', `?modal=true`); 
  };

  return (
    <div>
      <button onClick={openModal}>모달 열기</button>
    </div>
  );
}

LinkuseRouter를 사용하면 변경된 URL에 대한 탐색이 발생해 렌더링 로직이 다시 실행된다. 반면 History API를 사용하면 Next.js의 라우팅 시스템을 거치지 않고 URL만 변경하므로 렌더링 로직이 실행되지 않는다. 동시에 Next.js가 useSearchParams, usePathname과 동기화를 지원하기 때문에, URL 변화를 감지해 모달을 띄울 수 있다.

서버 컴포넌트는 Next.js에서 중요한 렌더링 최적화 전략이지만, 잘못 사용하면 의도치 않은 리렌더링이나 성능 저하를 일으킬 수 있다. 서버 컴포넌트를 효과적으로 사용하려면 이러한 렌더링 전략들을 잘 이해하고 적절히 활용하는 것이 중요할듯 하다.

전체 글 보기