[Next.js] React Server Component

giyeon·2024년 1월 28일
7

React

목록 보기
2/2
post-thumbnail

Next 13 업데이트에 app directory가 되면서, app directory 내부 모든 컴포넌트는 기본적으로 '서버 컴포넌트'로 동작하게끔 설정되었습니다.

app directory 내부에서 '클라이언트 컴포넌트'로 지정하고 싶다면, 아래와 같이 파일 최상단에 'use client' directive를 명시해야 합니다.

'use client' // HERE !
 
import { useState } from 'react'
 
export default function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

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

RSC는 React 18버전 부터 등장한 완전히 새로운 개념입니다. RSC를 통해 서버에서 실행되는 컴포넌트를 생성할 수 있습니다.
React 18 이전 버전에서는 특별한 설정이 없으면 모든 컴포넌트가 RCC로 구현되었습니다.
그러므로 React 18 버전을 다루려면 RSC와 RCC의 개념을 이해해야 합니다.

RSC와 RCC의 가장 큰 차이점은 렌더링(컴포넌트 실행)을 어디서 하느냐의 차이입니다.
RSC는 서버에서 실행되어 해석한 결과물을 클라이언트로 전달하고, RCC는 클라이언트가 서버로부터 javascript 번들 파일을 받아 렌더링을 하게 됩니다.

Next.js 공식문서에서 나와있듯, 위 표를 통해 RSC와 RCC는 명확하게 역할이 구분되어 있습니다. 그렇기 때문에 언제 어디서 RSC 또는 RCC를 사용할 것인지에 대한 결정이 중요합니다.

Next.js 프로젝트에서 컴포넌트 트리는 RSC와 RCC가 결합된 형태로 개발될 수 있습니다.
컴포넌트 트리구조에서 RSC 컴포넌트는 서버에서 렌더링을 하게되고, RCC는 클라이언트 측에서 렌더링이 되게 됩니다.

RSC의 렌더링 원리


위 그림은 RSC와 RCC가 혼합되어 컴포넌트 트리가 구성된 상태라고 가정합니다. 사용자가 페이지에 진입하면 필요한 데이터를 서버에 요청하게 됩니다.
요청을 받은 서버는 위 컴포넌트 트리를 최상단인 Root부터 실행하여, 최종적인 JSON 데이터 생성을 위해 각 컴포넌트에 대해 직렬화작업을 수행합니다.

직렬화 ?
직렬화는 데이터나 객체의 상태를 다른 환경 ( 파일, 메모리, 네트워크 )에 저장하거나 전송하기 위해 특정 형식으로 재구성하는 것을 말합니다.

직렬화 작업은 컴포넌트 트리 내에 있는 모든 RSC가 JSON객체 형태의 트리로 재구성될 때까지 수행됩니다.

// JSX 
<div style={{backgroundColor:'green'}}>hello world</div>

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

// JSON
> {
  $$typeof: Symbol(react.element),
  type: "div",
  props: { style:{backgroundColor:"green"}, children:"hello world" },
  ...
}

다만, 직렬화는 모든 컴포넌트에 대해서 수행되지 않으며 오로지 RSC 만 직렬화를 수행합니다.
직렬화작업 도중 RCC를 만나게되면 간단한 place-holder표시만 해두고 다음 트리항목으로 넘어가게 됩니다. 나중에 클라이언트 측에서 RCC를 올바른 곳에 위치시키도록 하기 위함입니다.

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

place-holder방식은 위와같이 함수를 직접 참조하는 방식이 아닌, module reference라는 새로운 타입을 적용하고 해당 컴포넌트의 경로를 명시하여 직렬화를 우회하게 됩니다.

RCC는 다음과 같은 이유로 직렬화 과정을 거치지 않습니다.

  • 클라이언트 사이드 로직 : RCC는 클라이언트에서만 실행되는 로직 (사용사의 상호작용, 상태관리, 브라우저 API 등)을 포함하고 있습니다. 당연하게도 이러한 로직은 서버에서 미리 처리될 수 없습니다.
  • 클라이언트의 환경 : 사용자의 브라우저마다 환경과 상태가 다르기 때문에 RCC는 브라우저 최적화되어 실행되어야 합니다. 이를 위해 직접적인 클라이언트 사이드의 스크립트 실행이 필요합니다.
  • Hydration : SSR환경의 페이지의 경우 RCC는 클라이언트 사이드에서의 수화과정이 필요합니다. 수화는 서버에서 생성된 정적 마크업을 클라이언트에서 동적으로 활성화하는 과정이며, 이 과정에서 RCC는 필요한 클라이언트 사이드 로직을 실행해야합니다.

결론적으로, RCC는 클라이언트 사이드에서의 동적인 상호작용과 로직 처리가 주된 목적이므로 이를 위해 서버측에서 직렬화 과정을 거치지 않습니다.

직렬화 과정을 마치게되면 아래 그림과 같이 JSON tree가 완성됩니다.

클라이언트는 직렬화 과정을 통해 생성된 최종 결과물을 스트림 형태로 전달받게 되고, 동시에 서버로부터 다운로드받은 javascript 번들을 참조하여 module reference 타입을 만날 때마다 올바른 RCC를 렌더링하여 최종 DOM에 반영하게 됩니다.

RSC와 RCC의 혼합 패턴

아래와 같이 RSC는 RCC 하위에 직접적으로 명시할 수 없습니다.

'use client'
 
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
 
export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
 	  /* 이러한 구성은 불가능합니다. */
      <ServerComponent />
    </>
  )
}

대신, RCC에 prop 형태로 넘겨주는 패턴은 가능합니다.
아래와 같이 RSC를 RCC의 children형태로 넘겨주게 되면 RCC는 해당 prop이 RSC인 것을 모른채, 단지 설정한 위치에 배치할 뿐입니다.

// app/parent-component.tsx (server component)

// This pattern works:
import ClientComponent from './client-component'
import ServerComponent from './server-component'
 
// Pages in Next.js are Server Components by default
export default function ParentComponent() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

/* 
* ServerComponent는 ClientComponent의 자식이긴 하지만, 
* ParentComponent가 공통부모이기 때문에 Parentcomponent가 렌더링 되는 시점에
* ServerComponent 또한 렌더링되어 결과값이 ClientComponent에 넘겨지게 된다.
* (ServerComponent는 서버에서 ClientComponent보다 미리 렌더링 되는 것)
*/
// app/client-component.tsx
'use client'
 
import { useState } from 'react'
 
export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      {children}
    </>
  )
}

위와 같이 props를 통해 RSC를 넘겨주게되면 RSC 와 RCC 가 분리되어 독립적으로 렌더링을 수행할 수 있게됩니다. 이 경우 child 인 RSC는 RSC가 클라이언트 측에서 렌더링 되기 전에 서버에서 미리 렌더링 될 수 있습니다. 공통 부모가 렌더링 되는 시점에 RSC가 실행이 되고(직렬화 되고), 그 결과값을 children으로 전달할 수 있게되어 서버에서 처리가 가능해지는 것입니다.

앞서 설명하였듯이, 서버환경에서는 RCC는 실행되지 않기 때문에 RCC에서 반환되는 RSC는 서버 컴포넌트 임에도 불구하고 실행되지 못합니다. 이러한 경우 RCC가 return 하는 RSC는 RCC와 동일하게 클라이언트에서 동작하게 됩니다.

RSC vs SSR

그렇다면 RSC 와 SSR은 같은 개념일까요?

컴포넌트가 서버에서 해석되어 어떠한 결과물을 클라이언트로 전달한다는 것은 같지만, 이 둘은 이루고자 하는 목표가 다르고 결과물마저 다릅니다. 둘 중 하나를 선택하여 적용한 다는 개념이 아니라, 별개의 개념으로 적용이 가능한 것입니다.

RSC에서의 렌더링과 SSR에서의 렌더링은 서로 다른 과정을 의미합니다.

프로젝트에서 작성한 소스코드가 브라우저에 보여지기 위해서는, 우선 컴포넌트가 실행되어 데이터가 해석되어야하고 그 해석된 데이터가 다시 HTML로 변환하는 과정을 거쳐야합니다.
컴포넌트가 실행되어 데이터를 해석하는 단계는 RSC과정이고, 해석된 데이터를 HTML로 변환하는 것은 SSR 과정이 됩니다.

필요에 따라 RSC와 SSR을 함께 사용하면, 큰 시너지를 낼 수 있게 됩니다.

*RSC에서의 렌더링

  • RSC 에서의 렌더링은 서버측에서 React 컴포넌트의 로직을 실행하는 과정을 말합니다.
  • 목적은 최종 HTML을 생성하는 것이 아닌, 컴포넌트의 로직을 처리하고 그 결과를 생성하는 것입니다.
  • 생성된 결과는 클라이언트로 '스트림' 형태로 전송되어 클라이언트에서 최종적으로 사용자에게 보여질 HTML로 변환됩니다.
  • RSC는 서버에서 데이터를 가져오거나 다른 서버 사이드의 로직을 처리하여, 클라이언트에 전송되는 데이터와 JS 번들의 양을 최소화 하는 데 초점을 둡니다.

*SSR에서의 렌더링

  • SSR에서의 렌더링은 서버에서 React 컴포넌트를 사용하여 완전한 HTML 페이지를 생성하는 과정을 말합니다.
  • SSR은 서버에서 시작되어 서버에서 완료되며, 생성된 최종 HTML은 클라이언트로 전송됩니다.
  • 클라이언트가 HTML을 받으면 React는 ‘Hydration’과정을 통해 수신한 HTML을 추가적인 클라이언트 사이드 동작을 활성화 시킵니다.
  • SSR은 초기 페이지 로딩 시간을 단축하고, SEO를 향상시키는 데 초점을 둡니다.

RSC + SSR

RSC와 SSR을 결합시, 다음과 같은 과정을 거치게 됩니다.

  1. RSC 과정
    • 서버에서 RSC로 정의된 컴포넌트의 로직을 실행합니다. 이 과정에서 data fetcing, 계산 및 상태관리 등이 처리됩니다.
    • HTML을 생성하는 것이 아닌 컴포넌트 구조와 필요한 데이터를 결정합니다. (직렬화)
  2. SSR 과정
    • RSC에서 처리된 컴포넌트와 데이터를 사용하여 SSR 과정으로 넘어오게 됩니다.
    • RSC 처리된 데이터를 사용하여 HTML를 생성합니다.
    • 생성한 HTML 을 클라이언트에게 전송합니다.
  3. 클라이언트에서의 렌더링
    • 클라이언트는 완성된 HTML을 받아 페이지를 렌더링하게되고, 필요시 React는 클라이언트 인터렉션 활성화를 위해 ‘Hydration’과정을 거치게 됩니다.

그렇다면, 아래와 같은 궁금증이 생길 수 있습니다.

RCC도 SSR을 적용하면 클라이언트가 받는 것은 HTML일텐데 RSC든 RCC 든 SSR을 적용하면 HTML을 받는 것은 동일한 것 아닌가 ? SSR을 사용한다고 가정했을 때, RSC 이점은 무엇인가?

결론부터 말하자면, RSC 와 SSR을 결합하게 된다면 클라이언트 측에서 수화과정이 필요없어지거나 최소화 되게 됩니다. 클라이언트 측에서 처리해야할 작업이 줄어들게 되는 것입니다.

RSC 의 직렬화된 데이터를 바탕으로 SSR 과정에서 HTML을 생성하기 때문에 클라이언트는 추가적인 수화과정이나 자바스크립트실행 없이 단순히 HTML을 렌더링만 하면 될 뿐입니다.

반면, RCC 와 SSR 결합시 SSR을 통해 받은 HTML을 클라이언트 사이드의 상호작용과 동적기능을 처리하기 위해서 수화과정을 반드시 거치게 됩니다.

즉, RSC와 SSR 을 결합하여 사용하게 되면 다음과 같은 이점이 있습니다.

  • 성능 최적화 : RSC를 통해 직렬화 과정을 거치게되고, 이를 통해 생성된 결과만 클라이언트로 전송합니다. 이는 클라이언트에서 실행되는 자바스크립트 코드의 양이 적다는 의미이므로 성능을 향상 시킬 수 있습니다.
  • 네트워크 개선 : 서버에서 데이터를 직접 처리하게 되므로, 데이터 전송량을 최적화할 수 있습니다. 클라이언트의 네트워크 부담을 줄일 수 있게되는 것입니다.
  • 보안개선 : RSC는 서버에서 실행되므로 클라이언트에 노출되지 말아야할 데이터나 로직을 보다 안전하게 처리가 가능해집니다.

참조
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

profile
Web , App developer wannabe 🧑🏻‍💻

3개의 댓글

comment-user-thumbnail
2024년 5월 7일

Rcc에 대해 오해의 소지가 있을 수 있는게 rcc 또한 서버에서 실행됩니다!

1개의 답글