해당 글은 What Are React Server Components (RSCs) and How Are They Different? 글을 번역하였습니다.
리액트 서버 컴포넌트(RSC)는 서버에서만 실행되는 리액트 컴포넌트이다. 리액트가 단순히 클라이언트(브라우저)에서만 실행된다고 여기는 것과는 다른 패러다임이다.
이 새로운 패러다임의 주요 차이점은 컴포넌트의 출력(output)과 어디서 실행되는지와 관계 있고 빌드 방식과 큰 연관이 없다.
리액트 앱을 만드는 방식에는 많은 변화가 있지만 생각하는 것만큼 극적이지는 않다. 이 글의 목적은 무엇이 변화했고, 왜 변화했고, 새로운 패러다임으로 리액트 앱을 만드는 것을 생각하는 방식에 대해 설명하는 것이다.
이 글은 완전히 오픈 소스이므로 어떤 것을 추가, 변경 또는 실수를 바로잡고자 한다면 해당 Github repo를 확인하면 된다.
서버에서 리액트 컴포넌트를 실행하는 것은 서버에서 리액트 컴포넌트를 pre-rendering 하고 클라이언트에서 수화(hydrate)시키는 Next.js나 Remix의 Server Side Rendering(이하 SSR)에 대해 들어봤다면 친숙할 것이다.
pre-redering은 기본적으로 리액트 컴포넌트 트리(pre-animaations, pre-hooks 등)를 리액트의 renderToString() 메소드를 사용해 HTML로 렌더링하고 브라우저로 보내는 것을 일컫는다.
HTML은 페이지가 애니메이션, 상태 변화, effects 등이 발생하기 이전에 페이지가 로드될 때 표시되는 첫 프레임의 문자열 스크린샷으로 생각할 수 있다. 예를 들면 아래와 같다.
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">
<div class="App">
<h1>Hello World!</h1>
</div>
</div>
</body>
</html>
이 HTML 문자열은 렌더링되기 위해 브라우저로 보내지고, 브라우저가 리액트와 모든 자바스크립트를 로드하면 리액트에 의해 수화된다.
서버에서 리액트 앱을 렌더링하는 Next.js와 Remix와 같은 프레임워크들은 잘 알려져 있지만 수화 단계가 남아 있기 때문에 리액트를 서버에서 실행하는 것과는 꽤 차이가 있다.
수화는 기본적으로 페이지를 인터랙티브하게 만들 수 있도록 모든 HTML과 사용자 브라우저에 필요한 JS를 리렌더링하는 것을 의미한다.
따라서 SSR에서 사용자가 스크린에서 콘텐츠를 보더라도 페이지를 아직 사용할 수 있다는 의미는 아니다.
리액트는 여전히 Virtual DOM을 실제 DOM에 바인딩하고, 이벤트 리스너 및 핸들러를 설정하는 것과 같은 작업들을 수행해야 한다.
리액트는 HTML이 화면에 표시되더라도 페이지 상의 요소들과 상호작용하기 이전에 네비게이션, 버튼 클릭 등과 같은 것들을 어플리케이션에 바인딩 시켜야 한다.
리액트는 hooks, state, effects, 클릭 핸들러 등이 존재하지 않는 완전한 정적 페이지더라도 여전히 페이지를 수화시킬 필요가 있다.
SSR이 해결한 문제는 수화 단계를 제거시키기 위해 리액트를 서버에서 실행시키는 것이 아닌 리액트가 모든 구성 요소를 로드하는 동안에 사용자로 하여금 콘텐츠를 볼 수 있도록 한 것이었다.
SSR을 사용하면서 여전히 수화 단계가 필요하다면 그것은 (return 구문 이전에 호출되는) 리액트 로직이 여전히 클라이언트에서 실행할 필요가 있음을 말한다. Next.js의 getServerSideProps나 Remix loader와 같은 해결책은 일단 무시하자.
당신의 컴포넌트와 그 로직이 여전히 클라이언트에서 실행되는 이유는 Babel이 클라이언트 상에서 JSX를 React.createElement() 호출로 트랜스파일 하기 때문이다.
이러한 호출은 당신의 컴포넌트를 묘사하고 브라우저에게 "이것들은 실제 DOM에서 우리가 추가/변경하고자 하는 Virtual DOM의 DOM 노드들이에요"라고 말하는 (JSON object 형태의) 리액트 요소들을 생성한다.
예를 들면, 우리가 오직 div를 포함하는 JSX 컴포넌트를 렌더링 시킨다고 해보자.
export default function HelloWorld() {
return <div>Hello World</div>
}
그러면 Babel은 이를 React.createElement() 호출로 트랜스파일하고 다음과 같은 결과를 도출한다.
{
"type": "div",
"props": {
"children": "Hello World"
}
}
따라서 어플리케이션을 서버 사이드 렌더링을 하더라도 첫 HTML 결과는 React.createElement() 호출의 결과로 뒤집어 씌어지고 HTML은 수화된 것으로 간주된다.
RSC와 SSR의 차이점은 SSR은 리액트 앱을 렌더링하는 데에 다음과 같은 과정을 갖는다는 점이다.
- 서버가 renderToString으로 JSX의 초기 HTML 스냅샷을 렌더링하고 이를 브라우저로 보내도록 한다.
- 컴포넌트 트리의 JSX를 서버에서 브라우저로 보낸다.
- 브라우저에서 React.createElement 메소드 호출로 (다시 DOM과 컴포넌트 트리의 JSON 표현인) JSX에서 리액트 요소를 생성한다.
- 이러한 리액트 요소들을 클라이언트 상의 HTML에 수화시키기 위해 DOM에 렌더링한다.
대신 서버 컴포넌트에서 브라우저로 보내는 것은 이미 서버에 의해 리액트 요소로 렌더링된 리액트 컴포넌트 자체를 전송한다.
즉, 브라우저는 JSX에서 직렬화된 JSON 출력을 서버에서 직접 가져오며, 클라이언트에서는 JSX를 트랜스파일할 필요가 없다.
서버 컴포넌트는 수화하지 않기 때문에 서버 컴포넌트가 로드되면 다소 정적으로 보일 수 있는데 이에 주목할 가치가 있다.
이는 Next.js 13의 app 디렉토리를 사용하는 어플리케이션을 보거나 실행했을 때 서버 컴포넌트는 브라우저를 필요로 하는 hooks(useState, useEffect)를 지원할 수 없다는 팝업 메시지를 보게 되는 이유이다.
서버 컴포넌트는 다음과 같기 때문이다.
- 서버 컴포넌트는 "무상태의" 컴포넌트로 간주되는데, 이는 자기 자신의 상태를 관리하지 않음을 의미한다. 그보다는 부모 컴포넌트로부터의 props나 자기 컴포넌트 내에서 data를 fetching 하는 것에 의존한다.
- 서버 컴포넌트는 오직 서버에서만 실행된다. 따라서 브라우저 API인
window나document는 작동하지 않는다. 서버에서는 브라우저의 개념이 없고, 단지 응답을 보내는 endpoint이기 때문이다.
서버 컴포넌트는 상태를 갖거나 브라우저 API에 접근할 수 없으므로 상태는 서버 컴포넌트에서 발생할 수 없고, effect가 작동하지 않는다. 이 컴포넌트들은 이미 수화될 수 없는 요소로 전달되기 때문에 DOM에 언제 마운트되고 업데이트되는지 알 수 없다.
만약 당신이 Rails나 PHP와 같은 전통적인 서버 사이드 프레임워크에 친숙하다면 이 개념이 익숙할 수 있고, 서버 컴포넌트가 어떻게 문제들을 해결하는지 궁금할 것이다.
그러나 여기서 주요 차이점은 데이터 변화가 전체 페이지의 reload를 유발하지 않는 다는 점이며, 이는 getServerSideProps와 remix의 loaders를 넘어서는 주요 개선점이다.
신선하지 않은(stale) 데이터를 가져오거나 대체하기 위해 전체 route를 호출하는 대신에 (즉, 전체 페이지가 getServerSideProps를 re-feching 하거나 php 경로로 다시 히트 시키는 것) 리액트 서버 컴포넌트 아키텍처는 애플리케이션이 컴포넌트 트리를 재실행하고 mutation 이후나 새로운 데이터가 사용 가능해진 이후에 reconciliation 과정으로 컴포넌트와 DOM 노드를 업데이트할 수 있도록 한다.
이것이 의미하는 바는 상태가 완전히 클라이언트 상에서 유지되고 따라서 만약에 사용자가 블로그 검색바에 무엇인가 타이핑을 하고 현재 사용자가 있는 블로그 포스트에 새로운 댓글이 스트리밍 되는 경우에 새로운 댓글들을 가져오기 위해 reload 할 필요가 없으며 검색바에 입력한 것들이 새로운 블로그 포스트로 이동하더라도 사라지지 않음을 의미한다(검색 바는 댓글과 블로그 포스트를 감싸는 layout의 부분이라고 가정하면).
서버 사이드 상에서 신규 또는 업데이트된 컴포넌트 트리는 직렬화 되어 브라우저로 보내지게 되어 클라이언트 상에서 비교 및 패치할 수 있도록 하며 불필요한 리렌더링이나 re-fetch는 발생하지 않는다.

전체 경로가 동적 데이터에 대해 책임을 가지는 대신 컴포넌트는 각자의 데이터/데이터 패칭에 대한 책임을 가질 수 있다.
다른 이점으로는 이러한 컴포넌트들이 서버에서 실행되므로 직접적인 DB 접근, 비공개적인 API 접근, 컴포넌트 안에서의 파일 시스템 접근과 같은 것들을 허용한다.
그러므로 각각 컴포넌트는 자신의 데이터에 책임을 갖는 것뿐만 아니라 클라이언트 사이드 데이터, API key 유출로부터 안전해졌다. 예전에는 반대로 프레임워크가 덜 적용되는 곳에서는 공개 API 엔드포인트로 데이터 패칭에 사용되는 effect를 자주 볼 수 있었다.
그래서 많은 사람들은 우리 모두가 아는 일반적인 React에 무슨 일이 생겼는지 궁금해 한다.
"일반적인" 리액트 컴포넌트는 여전히 존재 하지만 이제 "클라이언트 컴포넌트"로 부른다.
오직 이름이 바뀌었을 분 정확히 기존과 같고, 기능 관점에서 변화한 것은 아무것도 없다.
다음에는 RSC가 해결한 문제들에 어떤 것들이 있는지 알아보자.
유용한 포스트 잘봤습니다