IT/React & Next.js

[Next.js] Next.js의 SSR 과정과 React Server Component

땅일단 2025. 2. 9. 21:07

원티드 프리온보딩 챌린지 "Next.js 확실히 알고 레벨업 하기" 2일차 내용 정리

 

리액트 18 이전 버전 SSR의 문제

  • 데이터 페칭(모두 완료되어야 html을 만들 수 있음) -> html로 렌더링 -> js 파일 로드 -> hydration 과정이 필요하다.
  • Hydration(수화) : 척박한 html에 물을 뿌려주어야 사용자와 상호작용을 할 수 있다.
  • 여러가지 컴포넌트가 한 페이지에 표시될 때, 한 컴포넌트가 무거워서 js 로드에 오랜 시간이 걸린다면 다른 컴포넌트들도 완료되기 전까지 Hydration을 할 수가 없다. 즉 모든 js가 로드되기 전에는 페이지에서 아무런 상호작용을 할 수 없음.
// 모든 fetch가 끝나야 Hydration을 시작한다.

function getServerSideProps () => {
    const info1 = fetch("/...");
    const info2 = fetch("/...");
    const info3 = fetch("/...");
    
    Promise.all(info1, info2, info3);
    
    return {props: {
        info1: info1,
        info2: info2,
        info3: info3
    }};
}

 

리액트 18이 해결한 방법

  • Streaming HTML + Suspense 사용함.
  • 이전 버전에서는 페이지 단위로 모든 컴포넌트를 한번에 진행했으나, 18버전부터는 컴포넌트마다 개별적으로 위의 과정을 수행하는 것으로 변경됨. (동시성)

 

※ 참고: 자바스크립트의 동시성에 대해

  • 자바스크립트는 콜스택에 작업들을 넣고 순서대로 하나씩 처리하는 싱글 스레드 언어지만, 콜백을 사용한다면 비동기/논블로킹으로 비동기 작업들을 동시에 진행한다.
  • 상세과정 : WebAPI에서 병렬로 비동기 작업을 수행한 뒤, 완료되면 태스크 큐에 콜백을 넣는다. 그리고 이벤트 루프는 콜스택이 비어있다면 태스크 큐에 있는 콜백을 콜스택에 넣는다. 0초가 걸리는 setTimeout이라도 무조건 콜스택에 곧바로 들어간 작업(=동기)보다 늦게 결과를 뱉는데, 이것 때문이다.

 

React Server Component(RSC)

  • RSC란 서버에서 실행되는 React 컴포넌트이다.
  • Next.js 서버에서 RSC를 실행하면서 데이터 페칭 및 렌더링을 진행한다.
  • 렌더링 완료 후 생성된 HTML을 클라이언트로 전달한다.
  • 클라이언트는 Hydrate 과정 없이 HTML을 그대로 사용한다.
  • 장점 : 번들 파일로 만들어지지조차 않는다. 그래서 js 용량이 감소하고 성능이 높아진다.
  • 단점 : useState, useEffect, window 등 client 기능을 사용할 수 없다. 왜? js가 없기 때문. 쿠키 값을 받을 수는 있음.
  • 그래서 사용자 상호작용이 필요 없이 서버에서 데이터만 가져와서 보여주면 되는 부분은 RSC로 만들고, 상호작용이 필요하다면 클라이언트 컴포넌트를 사용한다. (RSC 안에서 클라이언트 컴포넌트 사용가능. 반대는 오류) - 아래 코드 참고
// LikeButton.tsx

"use client";

import { useState } from "react";

export default function LikeButton() {
  const [liked, setLiked] = useState(false);

  return (
    <button
      className={`px-3 py-1 rounded ${liked ? "bg-red-500 text-white" : "bg-gray-200"}`}
      onClick={() => setLiked(!liked)}
    >
      {liked ? "Liked" : "Like"}
    </button>
  );
}
// PostListWithLike.tsx

async function getPosts() {
  const res = await fetch("...");
  return res.json();
}

export default async function PostListWithLike() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.slice(0, 5).map((post: any) => (
        <li key={post.id} className="p-2 border-b flex justify-between items-center">
          <div>
            <h2 className="text-lg font-bold">{post.title}</h2>
            <p>{post.body}</p>
          </div>
          <LikeButton />
        </li>
      ))}
    </ul>
  );
}

 

직렬화

  • JSON.stringify()와 같은 것.
  • React만의 직렬화 방식을 사용한다. xml 형식 -> key-value 형식으로 직렬화.
  • 함수는 직렬화할 수 없다. 실행 컨텍스트(현재 실행되는 코드의 환경까지 담고 있는 복잡한 구조체)까지 직렬화할 수 없기 때문.

 

SSR의 장점

상호작용 할 수 없는 HTML을 빨리 보여줘서 UX를 개선

 

SSR + RSC

  • RSC는 SSR 방식의 렌더링을 서버에서 처리하게끔 해줌
  • 도메인으로 접속하여 API로 요청을 하고 응답이 오면 렌더링된 html을 전달받으므로 정적인 사이트가 표시된다.
  • (RSC가 아닌 경우) 그 다음, js를 다운로드하고 Hydrate 과정을 거친다. (RSC는 js가 없기 때문)
  • Hydrate 과정이 끝나면 상호작용이 가능하고, 여기까지 왔다면 렌더가 완료된 상태이다.
  • 그런데 RSC에서 API를 여러개 요청하는 경우에는 어떨까? 이미 HTML을 내려받았는데 두번째 API의 결과는 어떻게 처리할까? -> 그것을 가능하게 해 주는 것이 Streaming HTML.
  • 컴포넌트 각각 개별적인 실행을 원한다면 Suspense까지 사용해야 함. (예시는 아래 코드 참고)
async function RscMainContent(): Promise<Element> {
    const fetching = new Promise(...);
}

export default function Page(): Element {
    return (
        <main>
            <Suspense fallback={<div>loading</div>}>
                <RscMainContent />
            </Suspense>
            <Suspense fallback={<div>loading</div>}>
                <RSCSubContent />
            </Suspense>
        </main>
    );
}

 

 


서버라는 개념이 들어가서 헷갈리는 부분이니까 정확하게 알아야 코딩할 때 의아하지 않을 수 있다!