[Next.js] Server Component & Client Component, 렌더링 방식

Server Component와 Client Component

기본적으로 app 폴더 하위의 모든 컴포넌트는 서버 컴포넌트

넥스트에서 클라이언트 컴포넌트를 사용하려면 컴포넌트 파일의 최상단에 'use client' 예약어를 작성해줘야 함

 -> 따라서 useState, useEffect 같은 훅을 사용하려면 use client를 작성해줘야 했던 것

-> 또한 use client를 작성한 노드의 서브트리는 모두 클라이언트 컴포넌트로 취급된다.

 

클라이언트 컴포넌트와 서버 컴포넌트를 동시에 사용해야하는 경우에는?

1) useClient를 사용하는 컴포넌트를 하나 선언하고

2) 페이지인 서버 컴포넌트에서 위의 컴포넌트를 임포트해서 사용하면 된다

- 사용자와의 상호작용이 필요한 컴포넌트는 클라이언트 컴포넌트로

- 그렇지 않은 경우에는 서버 컴포넌트로 분리해서 사용할 수 있다

 

 

MAP와 SPA

MPA (Multi Page Application)

1) 전통 서버 사이드 렌더링 방식인 MPA로부터 웹 프론트엔드 개발이 시작됨

- 각 페이지마다 html 파일 별도로 존재

 

2) 문제점 - UX 저하

- 페이지 이동 및 렌더링 발생시 깜빡거리는 현상이 있고, 컨텐츠의 양에따라 페이지별 편차가 심해지게 됨

- 이로인해 React, Angular, Vue 등 SPA가 등장

 

SPA (Single Page Application)

1) 브라우저에서 동작하는 JavaScript를 이용해 동적으로 페이지, 컴포넌트 등을 렌더링하는 방식

- 서버로부터 #root div를 받고, 나머지 요소들은 JS 번들을 통해 완성

- 더이상 새로고침이나 깜빡거림없이 웹서비스 이용이 가능해 UX 크게 향상

 

2) 문제점

- 텅빈 div만 불러오기 때문에 JS의 평가, 실행까지 하얀 화면이 유저에게 노출

- 이를 보완하기 위해 Code Splitting (Lazy-Loading) 방법 제시

- 하나로 번들된 코드를 여러 코드로 나눠 당장 필요한 코드가 아니면 나중에 불러옴

- 문제의 근본을 해결하진 못함

 

 

4가지 주요 렌더링 기법

CSR (Client Side Rendering)

브라우저에서 JS를 이용해 동적으로 페이지를 렌더링하는 방식

 

1) 장점

- 한번 로드가 끝나면 사용자와의 상호작용이 빠르고 부드럽다

- 서버에게 추가 요청을 보낼 필요가 없어 사용자 경험이 좋다

- 서버 부하가 적다

 

2) 단점

- 첫 페이지 로딩시간이 길 수 있다 (TTV, Time To View)

- JS가 로딩되고 실행될 때까지 페이지가 비어있어 검색 엔진 최적화에 불리하다

 

SSG (Static Site Generation)

서버에서 페이지를 렌더링하여 클라이언트에게 HTML 을 전달하는 방식

최초 빌드시에만 생성된다

- 사전에 정적 페이지를 만들어둠

- 클라이언트가 요청하면 서버에서는 이미 만들어져있는 사이트를 바로 제공!

 

1) 장점

- 첫 페이지 로딩시간 (TTV)가 매우 짧아 사용자가 빠르게 페이지를 볼 수 있다

- SEO에 유리

- CDN (Content Delivery Network) 캐싱이 가능하다

 

2) 단점

- 정적인 데이터에만 사용할 수 있다

- 사용자와의 상호작용이 서버와의 통신에 의존하므로, 클라이언트 사이드 렌더링보다 상호작용이 느릴 수 있다

- 서버 부하가 클 수 있다

- 마이페이지처럼 데이터에 의존해 하면을 그려주는 경우 사용 불가

 

ISR (Incremental Static Regeneration)

SSG 처럼 정적 페이지를 제공

설정한 주기만큼 페이지를 생성한다

- ex. 주기가 10분이라면 10분마다 데이터베이스 또는 외부 영향 때문에 변경된 사항을 반영

 

1) 장점

- 정적 페이지를 먼저 제공하므로 사용자 경험이 좋으며, 콘텐츠가 변경되었을 때 서버에서 페이지를 재생성하므로 최신 상태를 어느정도 유지할 수 있다

- CDN 캐싱이 가능하다

 

2) 단점

- 동적인 콘텐츠를 다루기에 한계가 있을 수 있다. (실시간이 아니므로)

- 마이페이지처럼 데이터에 의존하여 화면을 그려주는 경우 사용 불가하다

 

SSR (Server Side Rendering)

next는 기본적으로 ssr

SSG, ISR처럼 렌더링 주체가 서버

클라이언트의 요청시 렌더링(html 제공)

 

1) 장점

- 빠른 로딩 속도(TTV)와 높은 보안성을 제공한다

- SEO 최적화에 좋다

- 실시간 데이터를 사용한다

- 마이페이지처럼 데이터에 의존한 페이지 구성 가능

 

2) 문제점

- CDN 캐싱 불가

- 사이트 콘텐츠가 변경되면 전체 사이트를 다시 빌드해야하는데, 이 과정에서 시간이 오래걸리고 서버 과부하가 올 수 있다

- 요청할 때마다 페이지를 만들어야 한다

 

hydration

TTI (Time to Interactive)

페이지가 완전히 상호작용 가능해지기까지 걸리는 시간을 의미

hydration

SSR 후 클라이언트 측에서 React 컴포넌트들이 작용할 수 있도록 재활성화하는 과정

서버에서 미리 렌더링된 HTML 을 보내고, 브라우저에서 js가 로드되면 리액트는 그 컴포넌트를 hydrate하여 다시 리액트 컴포넌트로 연결해 사용자와의 상호작용이 가능해진다

따라서 hydration 과정이 느리면 TTI가 길어질 수 있다!

 

CSR 에서의 Hydration

CSR에서는 모든 리액트 소스파일을 다운받아야 hydration이 되고, 그 이후에 화면을 볼 수 있다 (TTV 길어짐)

 

SSR에서의 Hydration

서버에서 사용자의 요청이 있을 때마다 페이지를 새로 그려 사용자에게 제공

1) pre-rendering

- 사용자와 상호작용하는 부분을 제외한 껍데기만 먼저 브라우저에 제공

- TTV 가 단축됨

 

2) hydration

- 이 과정이 일어나기 전까지는 껍데기만 있기 때문에 버튼 클릭 등 상호작용할 수 없음

- 인터렉션에 필요한 모든 파일을 다운로드 받은 후에 (hydration 후에) 인터렉션 가능해짐

- 이 간극인 TTI를 줄이는 것이 관건

 

 

렌더링 패턴 실습

실습 환경

- json-server 설치 (package.json에서 server: 'json-server --watch db.json --port 4000' 추가해 사용함)

- db.json에 데이터 추가

더보기

{
  "products": [
     {
      "id": 1,
      "handle": "fjallraven-foldsack-no-1-backpack",
      "availableForSale": true,
      "isNew": false,
      "title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
      "description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
      "descriptionHtml": "<p>Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday</p>",
      "options": [
        {
          "name": "Size",
          "values": ["One Size"]
        }
      ],
      "price": {
        "amount": "109.95",
        "currencyCode": "USD"
      },
      "images": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
      "seo": {
        "title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
        "description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday"
      },
      "rating": 4.2,
      "tags": ["men's clothing", "backpack"]
    },
    {
      "id": 2,
      "handle": "mens-casual-premium-slim-fit-t-shirts",
      "availableForSale": true,
      "isNew": false,
      "title": "Mens Casual Premium Slim Fit T-Shirts",
      "description": "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.",
      "descriptionHtml": "<p>Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.</p>",
      "options": [
        {
          "name": "Size",
          "values": ["S", "M", "L", "XL"]
        }
      ],
      "price": {
        "amount": "22.3",
        "currencyCode": "USD"
      },
      "images": "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg",
      "seo": {
        "title": "Mens Casual Premium Slim Fit T-Shirts",
        "description": "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing."
      },
      "rating": 4.2,
      "tags": ["men's clothing", "t-shirt"]
    }
  ],
  "carts": []
}

 

SSG

기본적으로 SSG 로 동작하기 때문에 따로 해줄 것이 없다!

빌드시 전체가 미리 생성되기 때문에 빌드와 실행을 해주면 끝

yarn build && yarn start

 

정적/동적 페이지로 생성된 페이지들을 각각 확인할 수 있다.

 

동적인 데이터를 반영해야한다면 서버 컴포넌트 내에서 바로 비동기 함수를 사용할 수 있다 (fetch 권장)

 

type Product = {
  id: number;
  availableForSale: boolean;
  title: string;
  description: string;
  images: string;
}

export default async function Home() {
  const response = await fetch('http://localhost:4000/products');
  const data: Product[] = await response.json();
  return (
    <div className='flex flex-col gap-10'>
      <h1>상품목록</h1>
      <div className="flex">
        {data.map(product => {
          return <div key={product.id} className='flex flex-col justfy-center items-center w-[200px] h-[200px]'>
            <img
                width={150}
                height={150}
                src={product.images}
                alt={product.title}
            />
            <div className='bg-amber-50 w-full'>
              <h3 className='font-bold'>{product.title}</h3>
            </div>
          </div>
        })}

      </div>
    </div>
  );
}

빌드 후 실행하면 정적 페이지를 확인할 수 있다.

즉, 이 페이지는 로드되며 데이터를 패치하는 것이 아니라 서버가 미리 완성해둔 페이지를 받아오는 것이다.

(db.json 의 데이터를 수정하고 새로고침해도 영향이 없다!)

 

SSR

사용자가 접속할 때마다 페이지의 데이터를 새로 받아온다.

SSG 구현에서 서버 fetch 요청에 cache: 'no-store'만 적용해주면 된다! (SSG는 기본값인 cache: 'force-cache')

const response = await fetch('http://localhost:4000/products', {
    cache: "no-store"
});

 

이제 정적 데이터가 아니기때문에 빌드 후에 db.json 데이터 수정 -> 새로고침시 데이터를 받아와 반영한다.

 

CSR

클라이언트 컴포넌트에서 렌더링을 할 때 사용한다.

확인해야할 것!

- 컴포넌트 상단에 'use client' 작성

- 클라이언트 컴포넌트에서는 바로 async-await을 사용할 수 없기 때문에 fetch 함수를 따로 만들고 useEffect에서 호출 및 state에 저장한다

 

import ProductList from "@/app/_components/ProductList";

export default async function Home() {
  return (
    <div className='flex flex-col gap-10'>
      <h1>상품목록</h1>
        <ProductList/>
    </div>
  );
}
'use client'
import {useEffect, useState} from "react";

type Product = {
    id: number;
    availableForSale: boolean;
    title: string;
    description: string;
    images: string;
}

const fetchData = async () => {
    const response = await fetch('http://localhost:4000/products', {
        cache: "no-store"
    });
    const data: Product[] = await response.json();
    return data;
}

const ProductList = () => {
    const [data, setData] = useState<Product[]>([]);

    useEffect(() => {
        console.log('render');
        fetchData().then(setData);
    }, []);

    return (<div className="flex">
        {data.map(product => {
            return <div key={product.id} className='flex flex-col justfy-center items-center w-[200px] h-[200px]'>
                <img
                    width={150}
                    height={150}
                    src={product.images}
                    alt={product.title}
                />
                <div className='bg-amber-50 w-full'>
                    <h3 className='font-bold'>{product.title}</h3>
                </div>
            </div>
        })}

    </div>)
}

export default ProductList;

클라이언트에서 렌더링하기때문에 브라우저의 콘솔에서 console.log를 확인할 수 있다.

 

ISR

SSR 방식에서 next: { revalidate: (원하는 초단위 시간) } 을 넣어주면 된다!

const response = await fetch('http://localhost:4000/products', {
    next: {
      revalidate: 3,
    },
});