React Server Components(RSC)

xxziiko·2024년 12월 22일

[Next.js]

목록 보기
2/2
post-thumbnail

React의 클라이언트 중심 렌더링의 한계

CSR 중심의 렌더링은 UI를 동적으로 생성하여 초기 로딩 이후 빠른 페이지 전환과 부드러운 사용자 경험을 제공하는 이점이 있지만 한계도 존재

1. 초기 로딩 속도 저하
초기 요청 시 HTML이 빈 껍데기 상태로 전달된 후 클라이언트에서 자바스크립트를 로드하여 실행하기 때문에 데이터 요청 및 렌더링까지 시간이 소요
2. SEO 문제
빈 HTML로 인해 검색 엔진이 페이지 내용을 제대로 색인하지 못하는 문제 발생
3. 자바스크립트 번들 크기 증가
CSR은 클라이언트로 많은 양의 자바스크립트 번들을 전송해야 하기 때문에 브라우저 로딩 시간을 늘리고 성능 저하를 유발


React에서 서버사이드 렌더링 구조의 문제점

1. 자바스크립트 번들 크기가 0인 컴포넌트를 만들 수 없다.

	import snitizeHtml from 'sanitize-html' //206k (63.3k 	gzipped)

	function Board({text} :{text: string}) {
  	const html = useMemo(() => sanitizeHTml(text), 	[text])
  
  	return <div dangerouslySetInnerHtml={{__html: html}} 	/>
	}

이 컴포넌트는 63.3kb에 달하는 sanitize-html가 필요하다.
이렇게 외부 라이브러리가 무겁고 브라우저에서 로드 후 사용해야 하는 경우, 클라이언트의 성능이 저하되고 초기 로딩 속도가 느려질 수 있다.
👉 서버에서 해당 라이브러리를 실행하고 렌더링 결과를 클라이언트에 전달한다면?


2. 백엔드 리소스에 대한 직접적인 접근이 불가능

리액트에서는 백엔드 데이터에 접근하기 위해 REST API와 같은 방법을 사용하는 것이 일반적이다. 편리하지만 백엔드에서 항상 클라이언트가 접근하기 위한 방법을 마련해놓아야 하는 불편함 존재
👉 클라이언트에서 직접 백엔드에 접근해 원하는 데이터를 가져올 수 있다면?


3. 자동 코드 분할(code split) 불가능

코드 분할이란 하나의 거대한 코드 번들 대신, 코드를 여러 작은 단위로 나누어 동적으로 지연 로딩함으로서 앱을 초기화 하는 속도를 높여주는 기법을 말한다. React에서는 주로 lazy를 사용하여 이를 구현해왔지만, 개발자가 모든 필요한 컴포넌트를 수동으로 lazy로 감싸야 한다는 번거로움이 존재
👉 서버에서 코드분할을 자동으로 수행한다면?


4. 연쇄적으로 발생하는 클라이언트와 서버의 요청을 대응하기 어려움

최초 컴포넌트의 요청과 렌더링이 끝나기 전까지 하위 컴포넌트의 요청과 렌더링이 끝나지 않는 점, 서버 요청이 지연됨으로써 컴포넌트 결과물에 의존하는 하위 컴포넌트들의 Loading 상태의 렌더링을 추가적으로 발생시키는 단점 존재
👉 이러한 작업을 서버에서 수행하여 지연을 줄인다면?


React Server Components(RSC)

  • React 팀은 이러한 한계를 개선하기 위해 React Server Components(RSC) 도입
  • 클라이언트의 부담을 줄이기 위한 서버 기반 렌더링
    일부 컴포넌트를 서버에서 렌더링하고, 클라이언트에 HTML로 전송하여 자바스크립트 번들 크기를 줄임 -> 성능 최적화
  • SEO와 초기 로딩 속도 개선
    서버에서 미리 렌더링된 HTML을 전달하여 검색 엔진 최적화 문제를 해결
  • 데이터 패칭과 렌더링의 결합
    서버에서 데이터를 패칭하고 이를 렌더링까지 수행함으로써 클라이언트와 서버 간의 불필요한 데이터 요청을 줄임
  • 자동화된 코드 분할
    RSC는 자동으로 코드분할(Code Splitting)을 수행하며, 필요한 컴포넌트만 로드해 효율을 높임

RSC 특징

  • 클라이언트 컴포넌트는 서버 컴포넌트를 import 할 수 없다.
    서버 환경이 브라우저에 존재하지 않으므로 서버 컴포넌트를 실행할 방법이 없기 때문에 컴포넌트를 호출할 수 없다.


자료 출처: 모던 리액트 Deep Dive

  • 리액트의 컴포넌트 트리는 그림과 같이 클라이언트 및 서버 컴포넌트가 혼재되어 있다. 이런 구조가 가능한 이유는?
    👉 ReactNode는 React 컴포넌트 간의 통합된 데이터 구조를 지원하며, 클라이언트 컴포넌트와 서버 컴포넌트를 동시에 관리할 수 있는 기반을 제공한다.
    즉, ReactNode가 서버와 클라이언트의 역할을 분리하여 하나의 트리 구조로 결합시키는 역할을 하며, 각 컴포넌트를 적절한 환경에서 렌더링 할 수 있게 한다.

  • 따라서 리액트 컴포넌트는 총 세가지가 존재

  1. 클라이언트 컴포넌트

    • 서버 컴포넌트, 서버 전용 훅이나 유틸리티 등 서버와 관련된 것들을 불러올 수 없다.
    • 서버 컴포넌트가 클라이언트 컴포넌트를 자식으로 가질 수 있으며, 이 경우 서버 컴포넌트가 먼저 렌더링한 결과물을 클라이언트 컴포넌트가 받아 삽입하여 보여준다.
    • 일반적인 리액트 컴포넌트와 특징이 같다.
    • 명시적으로 선언하는 방법은 맨 윗줄에 'use client' 선언
  2. 서버 컴포넌트

    • 요청이 오면 그 순간 서버에서 한 번만 실행하기 때문에 상태를 가질 수 없다. 따라서 상태를 가질 수 있는 리액트 훅(useState, useReducer와 같은 훅)을 사용할 수 없다.
    • 마찬가지로 렌더링 생명주기도 사용할 수 없다. (useEffect와 같은 훅을 사용할 수 없다)
    • effect나 state에 의존하는 사용자 정의 훅도 사용할 수 없다. (단, 의존하지 않고 서버에서만 제공할 수 있는 기능을 사용하는 훅이라면 사용 가능)
    • 서버에서 실행되기 때문에 window.document등에 접근할 수 없다.
    • 데이터 베이스, 내부 서비스, 파일 시스템 등 서버에만 있는 데이터를 async/await으로 접근할 수 있고 컴포넌트 자체가 async한 것이 가능하다.
    • 다른 서버 컴포넌트를 렌더링하거나 div, span, p와 같은 태그 요소를 렌더링하거나 클라이언트 컴포넌트를 렌더링할 수 있다.
  3. 공용 컴포넌트

    • 서버와 클라이언트에서 모두 사용이 가능하며 공통으로 사용할 수 있기 때문에 모든 제약을 받는 컴포넌트가 된다.
    • React는 기본적으로 모든 컴포넌트를 다 서버에서 실행 가능한 것으로 분류하고, 클라이언트 컴포넌트라는 것을 명시적으로 'use client' 선언

RSC 동작원리


자료 출처: 모던 리액트 Deep Dive

  1. 서버가 렌더링 요청을 받는다. 즉, 루트에 있는 컴포넌트는 항상 서버 컴포넌트이기 때문에 서버 컴포넌트를 사용하는 모든 페이지는 항상 서버에서 시작된다.

  2. 서버는 받은 요청에 따라 컴포넌트를 JSON으로 직렬화 한다. 이때 서버에서 렌더링 할 수 있는 것은 직렬화 해서 내보내고, 클라이언트 컴포넌트로 표시된 부분은 해당 공간을 플레이스 폴더 형식으로 비워두고 나타낸다. 브라우저는 이 후에 이 결과물을 받아서 다시 역직렬화한 다음 렌더링을 수행한다.

  3. 브라우저가 리액트 컴포넌트 트리를 구성한다. 브라우저가 JSON 결과물을 받았다면 이 구문을 다시 파싱한 결과물을 바탕으로 트리를 재구성해 컴포넌트를 만든다. 클라이언트 컴포넌트를 받았다면 클라이언트에서 렌더링을 진행, 서버에서 만들어진 결과물을 받았따면 이 정보를 기반으로 리액트 트리를 만든다. 이 트리를 렌더링하여 브라우저의 DOM에 커밋한다.


🧐 플레이스 홀더?

클라이언트와 서버 컴포넌트가 혼합된 환경에서 서버 컴포넌트의 렌더링이 완료되기 전까지 클라이언트에서 임시로 표시되는 UI


SSR과 RSC의 차이점이 뭘까?

서버 사이드 렌더링(SSR)

  • 응답받은 페이지 전체를 HTML로 렌더링 하는 과정을 서버에서 수행 한 후, 그 결과를 클라이언트에 전달한다.
  • 클라이언트는 전달받은 HTML을 바탕으로 하이드레이션 과정을 거쳐 서버에서 생성된 결과를 확인하고, 필요한 이벤트를 연결한다.
  • SSR의 주요 목적은 초기 인터렉션이 불가능한 상태에서도 정적인 HTML을 빠르게 제공하여 사용자 경험을 개선하는 것
    따라서, 서버 사이드 렌더링에서 HTML 로딩 이후의 자바스크립트 실행은 비용이 든다.

🧐 하이드레이션?

서버에서 미리 렌더링된 정적인 HTML에 Javascript를 적용하여 상호작용이 가능한 동적인 애플리케이션으로 만드는 과정

서버 컴포넌트(RSC)

  • 서버에서 컴포넌트를 렌더링한 결과를 HTML로 전송
  • 이 방식은 클라이언트로 Javascript 번들을 전송하지 않기 때문에 번들 크기를 줄여 성능을 최적화 가능
  • 데이터 처리와 렌더링이 모두 서버에서 수행되며, 클라이언트에서는 최소한의 데이터만 전송하기 때문에 네트워크 비용을 절감

Next.js에서의 React Server Components

fetch 도입

  • 13버전부터 모든 데이터 요청은 웹에서 제공하는 표준 API인 fetch를 기반으로 이루어짐
    따라서 getServerSideProps를 사용하지 않고 서버에서 직접 데이터를 불러오는 것이 가능해졌으며 컴포넌트가 비동기적으로 작동하는 것도 가능해졌다.
  • fetch 요청에 대한 내용을 서버에서 캐싱하며, 클라이언트에서 별도의 요청이 없으면 해당 데이터를 최대한 캐싱해 중복된 요청을 방지한다.

정적 렌더링과 동적 렌더링

  • 기존에는 서버에서 불러오는 데이터가 정적이라면 getStaticProps를 활용해 정적으로 페이지를 만들어 제공했었는데, Next 13부터는 CDN에서 캐싱하여 기존 서버 사이드 렌더링보다 더 빠른 데이터 제공
  • 13버전 이상부터는 정적인 라우팅에 대하여 기본적으로 빌드 타임에 미리 렌더링하여 캐싱해두고, 동적인 라우팅에 대해서는 서버에 매번 요청이 올 때마다 렌더링 하도록 변경되었다. 이때, fetch의 캐시 옵션을 활용하여 요청 결과를 캐싱한다.
  • next/header나 next/cookie와 같은 헤더 정보와 쿠키 정보를 불러오는 함수를 사용하게 된다면 해당 함수는 동적인 연산을 바탕으로 반환하기 때문에 정적 렌더링 대상에서 제외된다.

캐시와 mutating, Revalidation

  • fetch의 옵션으로 revalidate를 설정할 수 있으며, 이는 페이지에 revalidate라는 변수를 선언해서 페이지 레벨로 정의하는 것이 가능해짐을 의미.
  • 루트에 revalidate를 선언하면 하위에 있는 모든 라우팅에 적용되어 렌더링된다.
  • 캐시와 갱신이 이루어지는 과정
    • 최초로 해당 라우트에 요청이 올 때는 미리 정적으로 캐시해 둔 데이터를 보여줌
    • 이 캐시된 초기 요청은 revalidate에 선언된 값 만큼 유지
      만약 해당 시간이 지나도 일단은 캐시된 데이터를 보여준다.
    • 캐시된 데이터를 보여주는 한편, 시간이 경과했으므로 백그라운드에서 다시 데이터를 업데이트
    • 해당 작업이 성공적으로 끝나면 캐시를 갱신, 그렇지 않으면 캐시를 재사용
  • 캐시를 무효화 하고 싶다면 router에 추가된 refresh 메서드 사용 → 브라우저의 히스토리에 영향을 미치지 않고 오직 서버에서 루트부터 데이터를 가져와 갱신, 브라우저나 리액트의 상태에 영향을 주지 않음

스트리밍과 점진적 페이지 로딩

자료 출처: 모던 리액트 Deep Dive

스트리밍

HTML을 작은 단위로 쪼개서 완성되는 대로 클라이언트로 점진적으로 보내는 기법.

스트리밍을 이용하면 모든 데이터가 로드될 때 까지 기다려야 했던 과거의 방식을 개선하여 먼저 데이터가 로드되는 컴포넌트를 빠르게 보여주는 것이 가능하다. 이는 일부 기능만 로드되어도 사용자가 페이지에서 인터렉션이 가능하다는 것을 의미하며 최초 바이트까지의 시간(TTFB)과 최초 콘텐츠풀 페인팅(FCP)를 개선하는 것에 도움을 준다.

  • 경로에 loading.tsx 배치
    예악어로 존재하는 Loading 컴포넌트를 렌더링이 완료되기 전에 보여줌(Loading은 Suspense를 기반으로 만들어진 Nextjs의 규칙)
  • React Suspense 배치
    React에서 제공하는 Suspense를 배치하여 좀 더 세분화된 제어를 할 수 있음

ref.

모던 리액트 Deep Dive

0개의 댓글