[ 살펴보기 ] NextJS - Rendering

[ 살펴보기 ] NextJS - Rendering

·

6 min read

기존 Page route를 사용하다가 App route를 접할 때 가장 혼동스러운 부분 중 하나가 바로 Server component와 Client component의 분리일 것이다. 간단하게 표현하자면 Server component는 Server ( NodeJS )에서 component를 render하여 그 결과를 반환해주고 Client component는 우리가 기존에 사용하던 React Component와 같이 Client ( Browser )에서 component render 작업이 이루어진다

그렇다면 NextJS app route에서 client component는 100% client side에서만 동작할까? client component라는 이름이 무색하게 그렇지도 않다. 왜냐하면 NextJS에서 build 과정을 수행할 때 server component든 client component든 상관없이 모두 render되어 static generation 결과에 포함되기 때문이다 ( NextJS에선 build시 수행되는 static site generation 역시 SSR로 본다 )

이번 포스트에서 이 둘의 차이점과 NextJS build 결과에 대해 살펴보자

Server Component

App route에서 Server component를 쓰기 위해 개발자가 별도로 조치 해야할 사항은 없다. App route에서 사용되는 모든 component는 default로 server component로 취급되기 때문이다.

NextJS는 React가 제공하는 Server React DOM APIs를 통해 Server-Side에서 React component를 처리한다. NextJS 공식문서에 따르면 NextJS는 Server component를 React Server Component Payload ( RSC Payload )라는 data 형식으로 변환하고 RSC Payload를 이용해 server side에서 html을 생성한다.

그리고 생성된 html 파일은 ( before hydration ) 우선 사용자에게 전달되고 그 이후 Javascript code를 통해 client component들이 hydration 과정을 거치며 필요한 event handler들이 부착되고 비로서 각각의 component들은 사용자가 interact할 수 있는 interactive component가 된다. ( 아직 javascript가 적용되지 않아 interactive하지 않은 HTML Element들에 javascript code가 실행되며 각 element에 필요한 event가 추가되는 과정을 hydration이라고 한다 )

그리고 Server component가 render되는 방식은 다음 세 가지가 존재한다. Static Rendering, Dynamic Rendering, Streaming

Static Rendering

App route에서 Server component는 default로 Static rendering 방식으로 render가 된다. Static rendering이 적용된 route ( 특정 page )는 build시 각 페이지마다 필요한 html을 미리 생성하는 과정에 포함된다

특정 user 정보에 따라 변경되는 사항이 없는 페이지, 예를들어 게시판이나 상품 페이지 등이 Static Rendering을 적용하기 적합한 페이지의 예시라고 할 수 있다.

다음과 같은 페이지를 build한다고 가정해보자

// app/order/ssr/page.tsx

const OrderSSR = async () => {
  ...
  return (
    <div>
      <h3>OrderSSR</h3>
      ...
    </div>
  );
};

export default OrderSSR;

위의 예제에서는 기본적인 Server component를 사용하는 page이므로 static rendering이 적용된다. 그리고 build시 다음과 같이 static page가 생성되는 것을 확인할 수 있다

Dynamic Rendering

Dynamic rendering를 통해 render되는 페이지는 build time이 아닌 request time에 render가 이루어진다. 그렇기에 특정 user 정보에 따라 변경되어야 하는 페이지나 request time에 알 수 있는 정보를 통해 render를 수행해야 하는 page에 적용하기 보다 적합하다

다음 예제와 같이 force-dynamic route segment를 통해 page를 dynamic route로 전환하여 다시 빌드를 진행해보자

// app/order/ssr/page.tsx

export const dynamic = "force-dynamic";

const OrderSSR = async () => {
  ...
  return (
    <div>
      <h3>OrderSSR</h3>
      ...
    </div>
  );
};

export default OrderSSR;

위의 결과에서 볼 수 있듯이 Static Rendering과는 다르게 page의 static 파일 자체를 생성하지 않으며 해당 page의 render는 request time에서 이루어 진다. Page route를 접해봤다면 getServerSideProps의 역할을 App route에서는 dynamic rendering이라는 방법을 통해 한다고 생각하면 이해하기가 보다 수월하다

force-dynamic route segment의 사용 이외에도 NextJS에서 제공하는 cookie()나 header()와 같은 dynamic function의 사용여부에 따라 특정 페이지의 render가 dynamic rendering으로 전환될 수 있다.

특정 페이지가 Dynamic render 되는 경우는 대략 다음과 같다.

  • NextJS에서 제공하는 dynamic functions을 사용할 때, child component에서 사용해도 전체 route가 dynamic render 처리된다.

  • dynamic route segment가 'force-dynamic'으로 설정되어 있을 때 ( page의 component가 아닌 page level에 선언되어 있어야 한다 )

  • app route에서 page가 default로 받는 props 중 searchParam prop을 페이지에서 사용하면 해당 페이지는 dynamic render 처리된다.

  • server component에서 NextJS에서 제공하는 cookie나 header와 같은 dynamic function을 사용하는 server action을 호출하면 해당 page는 dynamic render 처리된다.

Streaming

특정 사이트를 처음 접근하거나 브라우저의 refresh가 발생하는 상황과 같이 full page load가 발생하면 해당 페이지에 필요한 html을 server에서 처리하여 client에 전달하게 된다.

이때 페이지를 구성하는 특정 UI block에서 data를 fetch하는데 시간이 오래 소요되거나 혹은 다른 작업으로 인해 UI block 처리 시간이 오래 걸린다면 전체 page loading 시간이 함께 느려지게 되는데 App route에선 이를 Suspense boundary를 통해 완화할 수 있다.

App route에선 Suspense boudnary로 감싼 UI block은 streaming을 통해 fallback에 정의된 static ui block을 우선 response로 보내고 이후 준비가 완료된 UI block을 client로 전달하여 Suspense fallback을 원래 UI block으로 업데이트 한다.

페이지 전체 html을 모두 한번에 준비해서 client로 전달하는 방식이 아닌 준비가 된 UI section을 우선적으로 전달하여 client에서 사용할 수 있도록 해준다.

특정 UI section을 streaming render로 처리하기 위해선 React Suspense로 특정 UI section을 감싸준다. 다음 예제를 살펴보자


const OrderSSR = async () => {
  return (
    <div>
      <h3>OrderSSR</h3>
      <OrderTitle />
      <OrderList />
    </div>
  );
};

export default OrderSSR;

만약 위의 페이지에서 OrderList라는 component에서 api request를 하고 있고 해당 request의 response를 받는데 5초 정도가 소요된다면 page 전체가 5초 뒤에 보이게 된다

import { Suspense } from "react";

const OrderSSR = async () => {
  return (
    <div>
      <h3>OrderSSR</h3>
      <OrderTitle />
      <Suspense fallback={<div>loading...</div>}>
        <OrderList />
      </Suspense>
    </div>
  );
};

export default OrderSSR;

하지만 위의 예제처럼 처리하는데 시간이 소요될 수 있는 UI Section을 Suspense로 감싸면 나머지 UI Section과 Suspense의 fallback component가 우선 보이게 되고 data fetch가 모두 끝나 UI section이 준비되면 Suspense의 fallback component는 OrderList component로 변경되어 사용자는 온전한 페이지를 확인할 수 있게된다.

Client Component

Client component는 그 동안 우리가 흔히 사용해왔던 component라고 생각하면 되겠다. Client component는 build시 bundle file에 포함이 되며 NextJS App route에서 client component를 사용하기 위해선 다음과 같이 파일 상단에 'use client'라는 텍스트를 추가해주어야 한다

"use client";
import React from "react";

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

export default OrderTitle;

주의할 점은 비록 이름은 client component이지만 모든 경우에 client side에서만 실행되지는 않는다. 우선 NextJS application을 build할 때 NextJS는 server component, client component 관계없이 static html를 미리 생성한다.

그리고 full page load ( 사이트에 처음 방문하거나 browser refresh 발생 등 )가 발생했을 때 server component, client component 상관없이 해당 component의 html은 server에서 생성되어 넘어온다. 그렇게 넘어온 html을 사용자에게 우선 보여주고 그런 다음 해당 페이지와 관련된 javascript file을 받아 hydration 과정을 진행하고 사용자는 비로서 사이트를 정상적으로 사용할 수 있게된다

하지만 full page load가 아닐 때, 즉 Link와 같은 component의 클릭을 통해 client side navigation을 통해 특정 페이지로 이동했을 때 client component는 온전히 client side에서 render가 된다. 다시말해 특정 페이지가 필요한 javascript를 다운 받고 해당 javascript를 이용해 UI를 그리는 것이다. ( 우리가 흔히 알고 있는 Client Side Rendering 처럼 )

Composition Pattern

Client component를 Server component안에서 render해야 하는 상황이라면 단순히 client component를 server component안에 추가하면 된다. 하지만 그 반대 상황은 어떨까? 만약 server component를 client component안에서 render해야 하는 상황이라면 어떻게 해야할까?

아래의 코드를 살펴보자

"use client";
import React from "react";

const OrderClientComponent = () => {
  return (
    <div>
      <OrderServerComponent />
    </div>
  );
};

export default OrderClientComponent;

App route에선 위와 같이 server component를 client component내에 import하여 사용할 수 없다.

만약 Server component를 Client component 내부에서 사용하고 싶다면 Server component를 Client component의 prop으로 전달해야 한다


import React from "react";

const OrderList = () => {
  return (
    <div>
      <OrderClientComponent>
          <OrderServerComponent />
      </OrderClientComponent>
    </div>
  );
};

export default OrderList ;
"use client";
import React, { PropsWithChildren } from "react";

const OrderClientComponent = ({children}:PropsWithChildren) => {
  return (
    <div>
      {children}
    </div>
  );
};

export default OrderClientComponent;

위 패턴을 통해 Client component내부에 Server component를 전달하여 사용할 수 있다.