Next.js

[Next.js] 클라이언트 컴포넌트(Client Component)와 서버 컴포넌트(React Server Component) 정리

킹우현 2023. 8. 21. 02:34

Next.js 13과 서버 컴포넌트

next가 13버전으로 올라오면서 굉장히 많은 변화가 있었는데, 그 중 가장 파격적인 변화는 바로 app directory의 등장이라고 할 수 있다.

 

기존 next 프로젝트는 pages라는 폴더를 이용해 스크린간 라우팅을 셋팅할 수 있었는데, next13부터는 app 폴더가 pages 폴더를 대체하게 되었다.

 

그리고 app directory와 관련해서 우리가 집중할 부분은 app directory내부에서는 모든 컴포넌트가 기본적으로 서버컴포넌트로 동작한다는 사실이다.

"use client";

import {useState} from "react";

const ClientComponent  =() => {
 
 const [state,setState] = useState()

 return <div>Test</div>
}

export default ClientComponent

만약 app directory 내부에서 클라이언트 컴포넌트를 사용하고 싶다면 파일 최상단에 "use client"라는 directive를 명시해주면 된다.

 

RSC(React Server Component) vs RCC(React Client Component)

React Server Component(줄여서 RSC)는 React18부터 도입된 개념으로, 말 그대로 서버에서 동작하는 컴포넌트를 가리킨다. (이전 버전에서 우리가 사용하던 모든 컴포넌트가 바로 클라이언트 컴포넌트다.)

 

방금 언급했듯이 서버 컴포넌트와 클라이언트 컴포넌트의 가장 큰 차이점은 컴포넌트가 렌더링되는 장소가 서버냐 클라이언트냐의 차이다.

 

서버 컴포넌트는 서버에서 한차례 해석된 이후 클라이언트로 전달되고, 클라이언트 컴포넌트는 클라이언트가 js 번들을 다운로드 받은 후 해석하게 된다.

위 표에서 볼 수 있듯, RSC와 RCC는 렌더링되는 위치가 다르기 때문에 각각 할수있는 역할이 명확하게 구분되어 있다.

 

“즉 RSC가 새로나온 개념이니까 RCC보다 무조건 좋겠지?”라는 접근이 아니라 RSC와 RCC를 적재적소에 배치하여 개발하려는 접근이 필수적이라고 할 수 있다.

 

RSC의 동작 방식

RSC의 특징을 이해하고 RSC와 RCC를 적재적소에 배치하기 위해서는 실제로 RSC가 어떻게 렌더링되는지 이해할 필요가 있다. 아래와 같이 RSC와 RCC를 적절하게 혼합하여 구성한 스크린이 있다고 가정해 보자.

사용자는 해당 페이지를 띄우기 위해 서버로 요청을 날린다. 그러면 서버는 이때부터 컴포넌트 트리를 root부터 실행하며 직렬화된 json형태로 재구성하기 시작한다.

 

- 직렬화(Serialization) 이란 ? 네트워크 전송 시에 데이터를 전송하기 위해 값의 형태를 json과 같이 클라이언트와 서버 간 주고 받을 수 있는 형태로 데이터를 변환하는 것

ex) 직렬화를 수행하는 JSON.stringify() 함수와 역직렬화를 수행하는 JSON.parse() 함수

 

주의할점은 모든 객체를 직렬화할 수는 없다는 것이다. 대표적으로 function은 직렬화가 불가능한 객체다.

 

const a = 100;
    
const sample = ()=>{
    console.log(a)
}

sample() //100

function이 실행코드와 실행 컨텍스트를 모두 포함하는 개념이기 때문인데, 함수는 자신이 선언된 스코프에 대한 참조를 유지하고, 그 시점의 외부 변수에 대한 참조를 기억하고 있다. js의 클로저가 바로 이런 현상을 가리키는 용어이기도 하다.

 

이처럼 함수의 실행 컨텍스트, 스코프, 클로저까지 직렬화할 수는 없기 때문에 function은 직렬화가 불가능한 객체로 분류되는 것이다.

 

직렬화 과정은 모든 서버 컴포넌트 실행하여 json 객체 형태의 트리로 재구성할 때까지 진행된다. 예를 들면 다음과 같다.

<div style={{backgroundColor:'green'}}>hello world</div> //JSX 코드는 createElement의 syntax sugar다.

> React.createElement(div,{style:{backgroundColor:'green'}},"hello world")

> {
  $$typeof: Symbol(react.element),
  type: "div",
  props: { style:{backgroundColor:"green"}, children:"hello world" },
  ...
} //이런 형태로 모든 컴포넌트를 순차적으로 실행한다.

다만 이 과정을 모든 컴포넌트에 대하여 진행하는게 아니라, RCC일 경우 건너뛰게 된다. 하지만 RCC를 서버에서 해석하지 않고 건너 뛴다고해서 비워 둔다면 실제 컴포넌트 트리와 괴리가 생기게 된다.

 

따라서 RCC의 경우 직접 해석하는 것이 아니라 “이곳은 RCC가 렌더링되는 위치입니다”라는 placeholder를 대신 배치해준다.

 

{
  $$typeof: Symbol(react.element),
  type: {
    $$typeof: Symbol(react.module.reference),
    name: "default", //export default를 의미
    filename: "./src/ClientComponent.js" //파일 경로
  },
  props: { children: "some children" },
}

아까도 언급했듯이 RCC는 곧 함수이므로, 직렬화를 할 수 없다. 따라서 함수를 직접 참조하는 것이 아니라 “module reference” 라고 하는 새로운 타입을 적용하고, 해당 컴포넌트의 경로를 명시함으로써 직렬화를 우회하고 있다.

 

이제 이렇게 도출된 결과물을 Stream 형태로 클라이언트가 전달받게 되고, 함께 다운로드한 js bundle을 참조하여, module reference 타입이 등장할 때마다 RCC를 렌더링해서 빈 공간을 채워놓은 뒤, DOM에 반영하면 실제 화면에 스크린이 보여지게 되는 것이다.

 

RSC vs SSR(Server Side Rendering)

‘컴포넌트가 서버에서 해석되어 클라이언트로 전달되는게 바로 SSR아니야?’라고 생각될 수 있으나, 실제로 RSC와 SSR은 서버에서 처리한다는 공통점 외에는 각각 해결하고자하는 목표도 다르고, 일어나는 시점과 최종 산출물도 다른 완전히 별개의 개념이다.

 

따라서 반드시 둘 중 하나를 선택할 필요도 없고 필요에 따라 RSC와 SSR을 함께 사용하면 큰 시너지를 낼 수도 있다.

 

CSR과 Next.js의 SSR

Client Side Rendering은 말 그대로 클라이언트에서 컴포넌트를 렌더링하는 것을 의미한다.

 

서버에서 빈 html과 js bundle을 다운로드 받고, 이 js 소스코드를 클라이언트에서 해석해서 처음부터 그려나가게 된다. 때문에 SEO 측면에서 성능이 떨어지고 초기 로딩속도가 느리지만, 스크린간 이동이나 인터렉션에 강점이 있다.

 

반면 Server Side Rendering은 서버에서 컴포넌트를 해석하여 최종 결과물인 html 파일을 내려주는 것을 의미한다.

 

CSR과는 반대로 SEO 측면에서 우수하고 초기 로딩속도가 빠르지만, 페이지를 이동할때마다 새로운 html을 요청해서 받는 시간이 필요하고, 현재 화면에서도 작은 변경사항이 발생하면 처음부터 html을 다시 로드해야하기 때문에 스크린간 이동이나 인터렉션에 약점이 있다.

 

사실 next js에서 우리가 사용하는 ssr은 전통적인 의미의 ssr은 아니다.  ssr과 csr의 장점만을 취하기 위해 일종의 절충점을 찾은 형태라고 할 수 있다.

 

즉, 초기 로딩속도가 느리다는 CSR의 단점을 보완하기 위해 초기 로딩시에는 SSR을 통해 html파일을  빠르게 받아오고, 이와 병렬적으로 js번들도 함께 가져와서 미리 받아온 html과 병합하는 hydration과정을 거치는 것이다.

 

그 결과 빠른 로딩에 강점이 있는 SSR과 인터렉션에 강점이 있는 CSR의 장점을 모두 취할 수 있게 된다. 따라서 Next js의 SSR 뿐만 아니라 CSR의 특징도 많이 가지고 있으므로 RSC를 함께 사용했을 때 그 이점이 더욱 크게 극대화될 수 있다.

 

RSC의 장점

1) Zero Bundle Size

RSC는 서버에서 이미 모두 실행된 후 직렬화된 JSON 형태로 전달되기 때문에 어떠한 bundle도 필요하지 않다.

 

즉, RSC의 컴포넌트 소스파일 뿐만아니라, RSC에서만 사용하는 외부 라이브러리의 경우에도 번들에 포함될 필요가 없기 때문에 번들사이즈를 획기적으로 감량할 수 있다.

 

이러한 부분은 Next의 TTI(Time To Interactive) 개선에 크게 기여할 수 있는데, 이전에 살펴봤듯이 Next에서 SSR을 사용한다고 하더라도 초기 로딩속도에 이점이 있을 뿐 CSR과 동일한 사이즈의 js 번들을 다운받아야하기 때문에 TTI는 여전히 CSR 대비 큰 메리트가 없었기 때문이다.

 

하지만 RSC를 도입하면 다운로드 받아야하는 번들 사이즈가 줄어들게 되므로, TTI에 개선에 기여할 수 있다.

 

2) No More getServerSideProps / getStaticProps (app directory)

기존 next에서는 getServerSideProps / getStaticProps라는 함수를 이용해서 서버에 접근했었다.

 

때문에, Data fetch등을 수행할때는 반드시 getServerSideProps(or getStaticProps)함수를 page 최상단에서 수행하고, 이를 page에 prop으로 넘겨서 사용했어야 했다.

 

하지만 이 과정은 순수 React와는 괴리가 있어 처음 next를 사용하는 사람들에게 낯설 뿐만 아니라, 무조건 최상단에서 fetch 후 page에 prop으로 넘겨줄 수밖에 없는 구조 때문에, 실제 data를 사용하는 하위 컴포넌트의 depth까지 props drilling이 불가피했다.

 

반면 RSC는 그 자체가 서버에서 렌더링되므로, 컴포넌트 내부에서 Data Fetch를 실행해도 무방하다.

 

즉, data가 필요한 컴포넌트에서 직접 data fetch가 가능해졌고, next13의 app directory에서는 기본적으로 모든 컴포넌트가 RSC이기 때문에 더이상 getServerSideProps / getStaticProps는 불필요한 함수가 되었다.

 

3) Automatic Code Splitting

import dynamic from 'next/dynamic'
            
const DynamicComponent = dynamic(() => import('../components/hello'))

본래 code splitting을 하기 위해서는 React.Lazy나 dynamic import를 사용했어야했다.

 

하지만 RSC에서 RCC를 import 하는 케이스에서는 자동적으로 RCC를 dynamic import가 적용된다. 이 장점은 어떻게 보면 굉장히 당연한 사실인데, 서버에서 RSC가 렌더링될 때 RCC는 실행되지 않기 때문에 굳이 RCC를 즉시 import 할 필요가 없기 때문이다.

 

4) Progressive Rendering

위에서 살펴봤듯, next13부터는 컴포넌트가 서버에서 한차례 렌더링 되며, 그 결과물로 직렬화된 JSON이 생성된다고 했다. 그리고 client는 그 결과물을 스트림의 형태로 수신한다.

 

즉, 스크린의 모든 화면정보를 수신할 때까지 기다릴 필요 없이, 클라이언트는 먼저 수신된 부분부터 반영하기 시작하여 화면에 띄워줄 수 있게 된다.

 

따라서 RSC를 React.Suspense와 함께 사용한다면 모든 데이터를 기다릴 필요 없이 먼저 그릴 수 있는 부분을 반영하여 뷰를 로드한 뒤, data fetch가 완료되면 그 결과가 즉각적으로 스트림에 반영됨을 알 수 있다.

 

5) 컴포넌트 단위 refetch

전통적인 SSR의 경우 완성된 html파일을 내려주기 때문에 작은 변경사항이 발생하더라도 전체 페이지를 전부 새로 그려서 받아와야 했다.

 

하지만 직전에 설명했듯이 RSC는 그 최종 결과물이 html이 아니라 직렬화된 스트림 형태로 데이터를 받아오기때문에, 클라이언트에서 스트림을 해석하여 vitualDOM을 형성하고, Reconciliation을 통해 뷰를 갱신하는 과정을 거치게 된다.

 

즉, 화면에 변경사항이 생겨서 서버에서 새로운 정보를 받아와야하는 상황이 생기더라도, 새로운 스크린으로 갈아끼우는 것이 아니라 기존 화면의 state등 context를 유지한채로 변경된 사항만 선택적으로 반영할 수 있게 된다.

 

정리

Server Component

  • 기본적으로 생성하는 컴포넌트
  • 장점: 페이지 로드 시 자바스크립트가 아예 필요 없어서 빠르다.
  • 단점: html 안에 자바스크립트를 넣을 수 없다. ⇒ useState, useEffect, onClick … 등등 사용 불가능

Client component

  • 컴포넌트 파일 맨 위에 ‘use client’ 작성
  • 장점: html 안에 자바스크립트 마음대로 넣어서 기능 개발 가능
  • 단점: 페이지 용량도 커지고, 페이지 로딩 속도도 느려진다.
  • 페이지 로드 속도 느려지는 이유
    • html을 로드하고 나서 거기에 리액트 문법을 적용하기 위해 컴퓨터가 html을 분석하는 과정이 필요하다. (hydration)
    • 이것 때문에 페이지 로드 속도가 더 느려진다.

👉🏻 큰 페이지들은 보통 server component 로 만들고, 자바스크립트 기능이 필요한 특정 부분은 client component로 만들어서 넣는다 !

 

참고 : https://velog.io/@2ast/React-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8React-Server-Component%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0

 

Next) 서버 컴포넌트(React Server Component)에 대한 고찰

이번에 회사에서 신규 웹 프로젝트를 진행하기로 결정했는데, 정말 뜬금 없게도 앱 개발자인 내가 이 프로젝트를 리드하게 되었다. 사실 억지로 떠맡게 된 것은 아니고, 새로운 웹 기술 스택을

velog.io

https://www.halamlee.com/next-js-server-component-vs-client-component

 

[Next.js] server component vs. client component

 

www.halamlee.com