[ 살펴보기 ] NextJS - Dynamic Rendering

[ 살펴보기 ] NextJS - Dynamic Rendering

·

8 min read

NextJS application을 build할 때 default로 server component, client component는 모두 static rendering이 적용된다. 즉, build time에 페이지별로 static html file이 생성된다. 하지만 Next application을 build할 때 특정 경우에는 build time에 static html file을 생성하지 않고 client request time에 page를 생성해 return하는 dynamic render가 적용된다. NextJS에서 dynamic render가 적용되는 경우는 page에서 Dynamic API가 사용된 경우며 아래 예제를 통해 Dynamic render가 적용되는 경우를 살펴보자. Dynamic API의 모든 list는 documentation을 통해 확인할 수 있다. ( Documentation - Dynamic APIs )

다음 예제를 살펴보자.

app/gallery/page.tsx

import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";

const GalleryPage = () => {
  return (
    <div>
      <h3>Gallery Page</h3>
      <GalleryClientComp />
      <GalleryServerComp />
    </div>
  );
};

export default GalleryPage;

app/gallery/_sections/GalleryClientComp.tsx

"use client";

const GalleryClientComp = () => {
  return (
    <div>
      <h3>GalleryClientComp</h3>
    </div>
  );
};

export default GalleryClientComp;

app/gallery/_sections/GalleryServerComp.tsx

const GalleryServerComp = () => {
  return (
    <div>
      <h3>GalleryServerComp</h3>
    </div>
  );
};

export default GalleryServerComp;

위의 예제에서 GalleryPage라는 server component는 client component와 server component를 하나씩 import해서 render하고 있다. 그리고 위의 코드를 build 해보면 다음과 같이 static renderng이 적용되어 gallery page에 대한 static page가 생성되는 것을 확인할 수 있다.

.next/server/app/gallery.html

...
    <div>
      <h3>Gallery Page</h3>
      <div><h3>GalleryClientComp</h3></div>
      <div><h3>GalleryServerComp</h3></div>
    </div>
...

그렇다면 특정 페이지를 build time에서 static html을 미리 생성해두는 것이 아닌 client request time마다 새로 생성해서 전달할 수 있도록 dynamic rendering을 적용하고 싶을 때는 어떻게 해야할까? NextJS에서 특정 page에 dynamic render를 적용할 수 있는 방법에 대해 살펴보자.

NextJS에서 제공하는 cookies function을 사용하는 page는 dynamic rendering이 적용되어 build시 static html이 생성되지 않는다. cookies function은 server component에서만 사용이 가능하다.

app/gallery/_sections/GalleryServerComp.tsx

import { cookies } from "next/headers";

const GalleryServerComp = async () => {
  const cookieStore = await cookies();
  const testCookie = cookieStore.get("testCookie");
  return (
    <div>
      <h3>GalleryServerComp</h3>
      <div>{testCookie?.value}</div>
    </div>
  );
};

export default GalleryServerComp;

GalleryPage가 import하는 server component에서 위와 같이 cookie를 import하여 적용하고 다시 application을 build해보면 gallery page에 대한 static html file이 생성되지 않는 것을 확인할 수 있다. Page에 dynamic rendering이 적용되어 client request 발생 시 page가 dynamic하게 생성되어 제공되는 것이다.

NextJS에서 제공하는 cookie function에서 사용할 수 있는 method는 다음과 같다.

  • get : client request에 포함된 특정 cookie 정보를 return한다. read할 cookie의 name을 parameter로 전달한다.

      import { cookies } from "next/headers";
    
      const GalleryServerComp = async () => {
        const cookieStore = await cookies();
        const testCookie = cookieStore.get("testCookie");
        ...
      };
    
      export default GalleryServerComp;
    
  • gettAll : client reqeust에 포함된 모든 cookie list를 반환한다.

      import { cookies } from "next/headers";
    
      const GalleryServerComp = async () => {
        const cookieStore = await cookies();
        const testCookieArray = cookieStore.getAll();
        console.log(testCookieArray);
        ...
      };
    
      export default GalleryServerComp;
    
  • has : client request에 특정 cookie가 포함 되어 있는지 체크한다. 체크하고자 하는 cookie name을 parameter로 전달하고 client request에 해당 cookie가 포함되어 있다면 true를 return한다.

      import { cookies } from "next/headers";
    
      const GalleryServerComp = async () => {
        const cookieStore = await cookies();
        const testCookie = cookieStore.has("testCookie");
        ...
      };
    
      export default GalleryServerComp;
    
  • set : response header에 특정 cookie를 설정한다. cookie 설정은 server component가 아닌 server action 혹은 route handler를 통해 설정할 수 있으므로 주의하자.

      // app/api/cookie/route.ts
    
      import { cookies } from "next/headers";
    
      export async function GET() {
        const cookieStore = await cookies();
    
        cookieStore.set({
          name: "testCookie",
          value: "testCookieFromGallery",
          httpOnly: true,
          path: "/",
        });
    
        return Response.json({ success: true });
      }
    
  • delete : client request에 포함된 특정 cookie를 삭제한다. 삭제할 cookie name을 parameter로 전달한다. delete 역시 server component가 아닌 server action 혹은 route handler를 통해 설정할 수 있으므로 주의하자.

      // app/api/cookie/route.ts
    
      import { cookies } from "next/headers";
    
      export async function GET() {
        const cookieStore = await cookies();
        cookieStore.delete("testCookie");
        return Response.json({ success: true });
      }
    
  • toString : client request에 포함된 cookie list를 하나의 string으로 변환하여 return 한다.

      // app/gallery/_sections/GalleryServerComp.tsx
    
      import { cookies } from "next/headers";
    
      const GalleryServerComp = async () => {
        const cookieStore = await cookies();
        const cookieToString = cookieStore.toString();
        console.log(cookieToString);
        ...
      };
    
      export default GalleryServerComp;
    

Headers

NextJS에서 지원하는 headers function을 사용하는 page 역시 dynamic render로 처리된다. headers function을 통해 client request의 http header정보를 read할 수 있다.

app/gallery/page.tsx

import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";
import { headers } from "next/headers";

const GalleryPage = async () => {
  const headerList = await headers();
  const userAgentHeader = headerList.get("user-agent");
  return (
    <div>
      <h3>Gallery Page</h3>
      <div>{userAgentHeader}</div>
      <GalleryClientComp />
      <GalleryServerComp />
    </div>
  );
};

export default GalleryPage;

위의 예제와 같이 headers function을 적용하고 application을 build 해보면 해당 page는 dynamic render가 적용되어 static html이 생성되지 않는 것을 확인할 수 있다.

headers function은 read-only Web Headers object를 return하며 return된 object를 통해 사용할 수 있는 method는 다음과 같다.

  • entries : header의 모든 key/value를 탐색할 수 있도록 iterator를 return한다.

      import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
      import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";
      import { headers } from "next/headers";
    
      const GalleryPage = async () => {
        const headerList = await headers();
        const testList = headerList.entries();
    
        for (const keyValue of testList) {
          console.log({ key: keyValue[0], value: keyValue[1] });
        }
    
        return (
          <div>
            <h3>Gallery Page</h3>
            <GalleryClientComp />
            <GalleryServerComp />
          </div>
        );
      };
    
      export default GalleryPage;
    
  • forEach : header의 각 key/value를 대상으로 주어진 function을 실행한다.

      import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
      import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";
      import { headers } from "next/headers";
    
      const GalleryPage = async () => {
        const headerList = await headers();
    
        headerList.forEach((val, key) => {
          console.log({ val, key });
        });
    
        return (
          <div>
            <h3>Gallery Page</h3>
            <GalleryClientComp />
            <GalleryServerComp />
          </div>
        );
      };
    
      export default GalleryPage;
    
  • get : parameter로 전달 받은 header의 value를 return한다.

      import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
      import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";
      import { headers } from "next/headers";
    
      const GalleryPage = async () => {
        const headerList = await headers();
        const userAgentHeader = headerList.get("user-agent");
        return (
          <div>
            <h3>Gallery Page</h3>
            <div>{userAgentHeader}</div>
            <GalleryClientComp />
            <GalleryServerComp />
          </div>
        );
      };
    
      export default GalleryPage;
    
  • has : parameter로 전달 받은 header의 존재 여부를 boolean으로 return한다.

      import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
      import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";
      import { headers } from "next/headers";
    
      const GalleryPage = async () => {
        const headerList = await headers();
        const hasUserAgentHeader = headerList.has("user-agent");
    
        if(hasUserAgentHeader) {
          console.log("user-agent is included");
        }
    
        return (
          <div>
            <h3>Gallery Page</h3>
            <GalleryClientComp />
            <GalleryServerComp />
          </div>
        );
      };
    
      export default GalleryPage;
    
  • keys : header의 모든 key를 탐색할 수 있도록 iterator를 return한다.

      import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
      import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";
      import { headers } from "next/headers";
    
      const GalleryPage = async () => {
        const headerList = await headers();
        const headerKeyList = headerList.keys();
    
        for (const key of headerKeyList) {
          console.log({ key });
        }
    
        return (
          <div>
            <h3>Gallery Page</h3>
            <GalleryClientComp />
            <GalleryServerComp />
          </div>
        );
      };
    
      export default GalleryPage;
    
  • values : header의 모든 value를 탐색할 수 있도록 iterator를 return한다.

      import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
      import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";
      import { headers } from "next/headers";
    
      const GalleryPage = async () => {
        const headerList = await headers();
        const headerValueList = headerList.values();
    
        for (const key of headerValueList) {
          console.log({ value });
        }
    
        return (
          <div>
            <h3>Gallery Page</h3>
            <GalleryClientComp />
            <GalleryServerComp />
          </div>
        );
      };
    
      export default GalleryPage;
    

NextJS 15 version 부터 headers function은 asynchronous function이므로 promise를 return한다. 그러므로 NextJS 15 version을 사용한다면 headers function을 사용할 때 async/await syntax와 함께 사용해야 한다.

또한 headers function으로 return되는 object는 read-only object이므로 set이나 delete과 같은 method는 사용할 수 없다.

Route segment

NextJS app route에서 특정 페이지에 dynamic render를 적용할 수 있는 또 다른 방법은 route segment를 통해 해당 페이지를 dynamic render로 처리 한다는 것을 명시적으로 선언하는 것이다. dynamic render를 적용할 페이지에 다음과 같이 dynamic route segment를 force-dynamic으로 설정한 뒤 export 해준다.

import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";

export const dynamic = "force-dynamic";

const GalleryPage = () => {
  return (
    <div>
      <h3>Gallery Page</h3>
      <GalleryClientComp />
      <GalleryServerComp />
    </div>
  );
};

export default GalleryPage;

Dynamic routes

Dynamic routes가 적용된 page 역시 default로 dynamic render가 적용된다. app route의 구조가 다음과 같다고 가정해보자.

app
  /dashboard
    - page.tsx
  /order
    - page.tsx
    /[id]
       - page.tsx

위의 app route 구조를 기준으로 application build를 진행하면 dashboard와 order page는 static render가 적용되어 static html파일이 build 단계에서 생성되고 dynamic route가 적용된 order/[id] directory의 하위 page는 dynamic render가 적용된다. ( Dynamic route에 대한 자세한 사항은 documentation을 통해 확인할 수 있다 - Dynamic Routes )

app/order/[id]/page.tsx

import React from "react";

type Props = {
  params: Promise<{ id: string }>;
};

const OrderDetailPage = async ({ params }: Props) => {
  const orderId = (await params).id;
  return (
    <div>
      <h3>OrderDetailPage</h3>
      <div>{orderId}</div>
    </div>
  );
};

export default OrderDetailPage;

Dynamic route가 적용되는 page를 default로 dynamic render가 적용되지만 dynamic route의 일부 페이지에 static render를 적용해 build time에 static page를 생성하는 것은 가능하다. 예를 들어 database에 order 관련 data가 10개가 있고 각 order 관련 data의 id는 1부터 10까지라고 가정해보자. 여기서 dynamic route가 적용되는 order/[id] page 중에서 order id 1과 2에 해당 하는 페이지에 static render를 적용해 build time에 static page를 생성하고 싶다면 다음과 같이 generateStaticParams function을 사용하며 된다.

app/order/[id]/page.tsx

import React from "react";

type Props = {
  params: Promise<{ id: string }>;
};

export const generateStaticParams = async () => {
  return [{ id: "1" }, { id: "2" }];
};

const OrderDetailPage = async ({ params }: Props) => {
  const orderId = (await params).id;
  return (
    <div>
      <h3>Order Detail Page</h3>
      <div>{orderId}</div>
    </div>
  );
};

export default OrderDetailPage;

위의 예제에서 generateStaticParams는 dynamic route page가 param으로 전달 받는 id를 object array로 return하고 있으며 위의 code를 적용하고 build 해보면 .next/server/app/order 경로에 id 1과 2 value가 적용된 두 개의 static html가 생성되는 것을 확인할 수 있다. 즉, user가 /order/1 또는 /order/2 url로 접근하면 build시 생성된 static html이 바로 제공되고 /order/3으로 접근하면 dynamic render가 적용되므로 request time에 page는 생성하여 전달한다.

searchParam

dynamic route뿐만 아니라 searchParam을 통해 query string을 prop으로 전달 받는 page역시 dynamic route가 적용된다.

import GalleryClientComp from "@/app/gallery/_sections/GalleryClientComp";
import GalleryServerComp from "@/app/gallery/_sections/GalleryServerComp";

type Props = {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};

const GalleryPage = async ({ searchParams }: Props) => {
  const search = await searchParams;
  return (
    <div>
      <h3>Gallery Page</h3>
      <GalleryClientComp />
      <GalleryServerComp />
    </div>
  );
};

export default GalleryPage;

위의 page는 searchParam prop을 전달 받으므로 dynamic route가 적용되며 /gallery?page=1과 같이 url에 query param이 추가되면 searchParam prop을 통해 url에 포함된 query param을 조회할 수 있다.