6분
RSC Mental Model
React Server Component(이하 RSC)가 무엇인지, 또 그리고 어떻게 인식하고 사용하면 좋을지에 대한 Dan Abramov의 트위터 글을 옮겨보았습니다.
이 글을 정리하고 읽으면서 RSC에 대한 멘탈 모델을 조금이라도 얻을 수 있었으면 좋겠습니다.
RSC란 무엇인가요?
RSC는 React 18버전에서 소개된 새로운 형태의 컴포넌트입니다 이 컴포넌트는 서버에서만 렌더링되며, 클라이언트로 렌더링 결과물의 HTML이 특별한 구조로 전송됩니다. 사실 HTML 문서가 직접 전송되지 않고 컴포넌트 형태를 특별한 형태(serialize된 JSON Data)로 전송합니다. 그로 인해 전송 오버헤드를 줄이고 빠른 렌더링이 가능해집니다.
서버에서만 렌더링되기 때문에 클라이언트로 내려오는 JavaScript 번들 사이즈를 줄일 수 있습니다. 또한 서버에서 동작하는 컴포넌트이기 때문에, DB 접근이나 파일 시스템 접근 등의 서버 작업을 할 수 있습니다. 이로 인해 얻을 수 있는 장점이 많습니다.
- 성능 향상: RSC는 서버에서 렌더링되기 때문에, 초기 페이지 로딩 시 클라이언트에서 발생하는 렌더링 작업을 줄일 수 있습니다. 이로 인해 페이지 로드 시간이 단축되고 사용자 경험이 향상됩니다.
- 데이터 효율성: 서버에서 컴포넌트를 렌더링하면 데이터 가져오기(fetch)를 최적화할 수 있습니다. 필요한 데이터만 가져올 수 있으며, 클라이언트 사이드에서 추가 요청을 줄일 수 있습니다.
- 코드 분할: RSC를 사용하면 코드를 더 효율적으로 분할할 수 있습니다. 서버에서만 사용되는 코드를 서버 사이드 코드로 유지하고, 클라이언트 사이드 코드의 크기를 줄일 수 있습니다.
렌더링에 필요한 데이터를 위해 API 호출 필요 자체가 없어진다면 어떤지 생각해보신적이 있나요? RSC는 이런 것들을 가능하게 해줍니다.
React는 클라이언트 사이드 렌더링(CSR) 라이브러리로 많이 알려져 있는데 RSC는 그와 반대되는 것 아닌가요?
CSR의 문제점은 초기 페이지 로딩 시 클라이언트에서 발생하는 렌더링 작업이기 때문에, 초기 페이지 로딩 시간이 길어지고 사용자 경험이 저하된다는 것입니다. React도 이를 해결하기 위해 서버 사이드 렌더링(SSR)을 결합하여 초기 로딩 성능을 개선하고 SEO 등을 향상시키는 기법을 찾고 있었습니다. 그 중 하나가 RSC 입니다. 기존 SSR 방식과 달리, RSC는 서버에서 렌더링된 결과물을 정적 HTML이 아닌 데이터 구조 형태로 전송됩니다. 이 데이터 구조를 받은 클라이언트는 React 애플리케이션에 빠르게 렌더링 할 수 있습니다.
RSC는 초기 페이지 로드 및 데이터 효율성에 도움이 되는 반면에 클라이언트 컴포넌트는 상호작용 및 동적 기능에 필요합니다.
따라서 RSC는 CSR과 SSR의 장점을 결합한 것으로 볼 수 있습니다. CSR에 반대되는 개념이 아니라 기존 React의 클라이언트 사이드 렌더링을 보완하는 새로운 기능입니다.
We generally recommend using an existing framework, but if you need to build your own custom framework, it is possible. Building your own RSC-compatible framework is not as easy as we’d like it to be, mainly due to the deep bundler integration needed.
- react.dev/blog
RSC 호환 프레임워크를 직접 구축하는 것은 생각만큼 쉽지 않기에 현재 React 공식문서에서는 RSC를 사용하기 위해 일반적으로 프레임워크를 사용하기를 권장합니다.(Next.js는 RSC를 가장 잘 지원하고 있는 프레임워크 중 하나입니다.)
RSC 멘탈 모델
Dan Abramov의 트위터 설명과 질문응답을 통해 RSC에 대해 더 자세히 알아보겠습니다.
이는 서버 컴포넌트와 클라이언트 컴포넌트의 큰 차이점을 보여줍니다.
파란색 항목(서버 컴포넌트)은 서버에서만 동작하므로 DB, 파일 등을 읽을 수 있습니다.
초록색 항목(클라이언트 컴포넌트)은 클라이언트와 서버 모두에서 동작하므로 서버 전용 기능을 사용할 수 없고 리렌더링에서 실행되어야 합니다.
Remix와 비교하면, Remix에서는 loader
를 제외한 모든 것이 "초록색"(클라이언트 컴포넌트)입니다.
즉, 라우트 세그먼트 당 파란색 부분(서버 컴포넌트)은 하나만 가질 수 있고 더 아래로는 구성할 수 없습니다.
Astro와 비교하면, Astro는 파란색 부분(서버 컴포넌트)과 초록색 부분(클라이언트 컴포넌트)이 모두 존재하지만 서로 다른 언어로 작성되어 있습니다.(파란색 = Astro, 초록색 = React) 또한 페이지가 로드된 후 파란색 부분(서버 컴포넌트)를 리렌더링하는 기능(예: 네비게이션 또는 뮤테이션)이 지원되지 않습니다.
Rails(Turbolinks)와 비교하면, Rails에서 파란색 부분(서버 컴포넌트)는 refetch가 가능하지만, 다른 언어(templates)로 작성되어야 하고, 동기화되어야 하며(심지어 DB를 읽는 것도 동기화되어야 함), refetch 시에 초록색 부분(클라이언트 컴포넌트)의 상태가 손실될 수 있다고 생각합니다.
Dan Abramov의 질문응답
- Q1-1: 명확하게 말하자면, 중간 상태는 실제로 생성되거나 클라이언트로 전송되는 JSX 코드를 나타내는 것이 아니라 RSC만 렌더링되는 개념적인 상태라고 할 수 있나요? 기술적으로 모든 컴포넌트는 서버에서 렌더링되며 HTML을 직접 생성(suspense/streaming이 없는 경우)되는 것이 맞나요?
- Dan-1: 첫 번째 로드의 경우, 맞습니다. 새로고침의 경우 HTML으로 변환할 필요가 없으며(원하지도 않으며) 그렇게 하면 인플레이스 새로고침이 불가능해지므로(상태가 손실되기에) 트리는 JSON과 유사한 프로토콜을 통해 트리가 클라이언트로 전달됩니다.
- Q1-2: 그럼 새로고침할 때 서버에서 RSC가 리렌더링되지만 출력은 JSON으로 전성되고 클라이언트에서 VDOM 차이가 발생하는 것이 맞나요?
- Dan-2: 맞습니다.
- Q2-1: 페이지에 로더가 필요하다고 가정해보겠습니다. 로더는 어디에 위치해야 하나요? 로더가 표시되는 동안 서버가 렌더링할 내용을 기다리는 클라이언트 컴포넌트인가요? 이 경우 Suspense를 사용할 수 있나요?
- Dan-1:
이것을 의미하는 건가요? 양쪽 모두에서 작동합니다.
- Q3-1: 의문이 있습니다. ThemeToggle은 Context Provider이고 모든 하위 컴포넌트가 테마를 사용한다는 가정을 기반으로 합니다. 이것이 사실이라면, Post와 같은 서버 컴포넌트는 클라이언트 상태가 되므로 테마를 사용할 수 없을 것입니다. 맞나요?
- Dan-1: 맞습니다. 하지만 Post는 테마를 사용할 수 있는 클라이언트 컴포넌트(Text와 같은)를 렌더링할 수 있습니다.
- Q4-1: ThemeToggle을 클릭하면 서버 전용인 Post의 style props을 업데이트해야 하므로 항상 서버 렌더링으로 연결될 것 같습니다.
- Dan-1: ThemeToggle이 CSS 클래스/변수를 업데이트하거나(대부분의 경우 충분할 것입니다), Post 컴포넌트가 실제로 ThemeToggle이 제공한 Context를 읽는 디자인 시스템(예: Text)의 일부 클라이언트 컴포넌트를 렌더링한다는 가정이 맞습니다.
- Q4-2: 네. 또한 서버 컴포넌트는 클라이언트에 제공되지 않으므로 shouldComponentUpdate를 사용할 수 없습니다. 부모가 리렌더링하면 서버 자식 컴포넌트도 마찬가지입니다. 네트워크 라운드트립은 UI 렌더링보다 훨씬 더 많은 비용이 듭니다. 아마 일부 자식 컴포넌트들은 서버라는 것을 알고 있으므로 memo로 감싸게 될 것입니다.
- Dan-2: 아닙니다, 단방향 데이터 흐름 때문에 의도적으로 그런 일이 일어나지 않도록 설계했습니다.(항상 서버에서 시작됩니다) 클라이언트 부모 리렌더링은 자식 서버 부분을 "건너뛰게" 됩니다. 왜냐하면 이미 정적이기 때문입니다. 말 그대로 미리 계산된 트리 부분과 같습니다. 서버 콘텐츠를 새로 고치는 유일한 방법은 라우터 탐색이나 뮤테이션(form 제출과 같은)을 통해 명시적으로 요청하는 것입니다. 상태 업데이트로 인한 일반적인 리렌더링은 서버를 리프레시하지 않습니다. 이것이 바로 클라이언트 컴포넌트에서 서버 컴포넌트를 import할 수 없는 이유입니다. 물론 다른 부모로 감싸면 렌더링할 수 있지만, 이렇게 하면 클라이언트로 새로운 props를 전달할 수 없습니다. 서버 -> 클라이언트라는 한 방향으로만 렌더링됩니다.
- Q4-3: 알겠습니다. 그러니까 실제 서버 컴포넌트 대신에 동일한 자식을 렌더링하지만 props가 하드코딩된 "mock" 컴포넌트를 클라이언트로 전송하는 것과 같군요.
- Dan-3: 실제로 이런 이유로 많은 "디자인 시스템" 컴포넌트가 클라이언트이거나 클라이언트 파트를 가지고 있을 것입니다.
- Q5-1: RSC에 대해 궁금한 것 중 하나는 초기 로드가 서버에서 발생한다면 URL 변경이나 뮤테이션 없이 refetch를 어떻게 트리거할 수 있는지입니다.
예를 들어,
<Comments>
컴포넌트를 새로고침버튼을 통해 새로고침하려면, 이것은 HTTP 요청인가요? 아니면 RSC가 서브트리를 리렌더링할 수 있나요? - Dan-1: 클라이언트에서 개별 RSC를 "하나씩" 새로고침 할 수는 없습니다. 클라이언트는 출력된 렌더링 결과만 볼 수 있습니다. 개별적으로 지정할 수 없습니다. 대신, 어느 정도 수준(페이지 또는 섹션)에서 새로고침하게 될 것입니다. Next.js에서는 이것이(서브)라우트에 해당합니다.
- Q6-1: RSC
<Tweet />
컴포넌트를 사용하고 싶다고 가정해 보겠습니다. 그러나 일부 텍스트 위에 마우스를 올렸을 때 툴팁과 같이 클라이언트 사이드 상호작용이 발생한 후에만 사용자에게 표시하고 싶습니다. Next.js에서는 라우트 세그먼트를 통해 이 작업이 수행된다는 것을 알고 있지만 어떻게 수행되는지 명확하지 않습니다. - Dan-1: 문제가, 즉시 렌더링하지만 아직 표시하지 않으려는 건가요? 아니면 정말로 느리게 렌더링하는 건가요?
- Q6-2: 마우스를 올릴 때만 데이터를 가져오고 싶어서 그 경우만 렌더링하고 싶습니다. 그리고 fetch는 서버에서 이루어저야하고 결과는 스트리밍으로 전송되어야 한다고 생각하나요? 다시 말하지만 이 시나리오에서 데이터 흐름이 어떻게 발생하는지 잘 모르겠습니다.
- Dan-2: 이해가 되네요. 아직 임의로 lazy 렌더링하는 것에 대한 기본 지원은 없습니다. 앞으로 추가해야 할 것입니다. 가장 일반적인 경우는 라우트 탐색입니다. 탭을 변경하는 것처럼 라우터가 refetch된 세그먼트에 대해 RSC 트리의 일부를 요청합니다. 위 그림을 "Zoom out"했다고 상상해보세요. 중첩 라우팅을 지원하는 프레임워크(Next.js의 App Router같은)에서는 그림이 단일 라우트 세그먼트를 위한 것입니다. 라우터는 이들을 결합하여 독립적으로 새로고침 할 수 있습니다.
- Q7-1: 버전이 올라갈 때마다 React는 점점 더 복잡해지고 애플리케이션 개발자들이 이해하기 어려워지고 있습니다.
- Dan-1: 반대로 애플리케이션을 만드는게 더 간단해졌지만, 이해하는 데는 조금 시간이 걸릴 것입니다. 조그만 시간을 주세요. 우리는 이미 이런 일을 겪어봤습니다.
- Q7-2: 이해합니다. 다만 중급 개발자와 기술 리더에게 설명하기가 어려울 뿐입니다. 개발자들이 이전 버전에서 SSR과 Next.js의 서버/클라이언트 코드 바운더리를 혼동하는 것을 봅니다. 커스텀 SSR이 있는 레거시 React 코드베이스는... 행운을 빕니다.
- Dan-2: 네. 좋은 문서 없이는 힘듭니다. 두 그룹 모두를 위한 충분한 정보가 있으면 더 나아질 것이라고 생각합니다.
- Q7-3: 아멘. React 문서는 프론트엔드 개발자들 사이에서 가장 많이 읽히고 참조되는 문서일 것 같습니다. 많은 사람들이 문서를 훑어보고 코드 작성에 뛰어들고, 미지의 세계를 탐색하기 위해 구글에 의존합니다. 문서에 쏟는 노력에 감사드립니다. 🙏
Dan Abramov가 RSC에 대해 조금 더 이해하기 쉽게 퀴즈를 내고 답변을 달아주는 글인데 잘 정리되어 있어서 옮겨 보았습니다.
RSC Quiz 1
이 중 유일한 클라이언트 컴포넌트는 <Toggle />
입니다. 상태(isOn
, 초기값 false
)를 가지고 있습니다. 그리고 <>{isOn ? children : null}</>
을 반환합니다.
setIsOn(true)
를 하면 어떻게 될까요?
- (1)
<Details />
를 fetch한다. - (2) 아니면
<Details />
가 즉시 나타난다.
RSC Quiz 1 답변:
(2) <Details />
가 즉시 나타납니다.
모든 RSC는 서버에서 단일 패스로 실행되므로 (명시적으로 라우터 새로고침이나 네비게이션을 하지 않는 한) 앞뒤로 이동하지 않습니다.
따라서 <Toggle />
클라이언트 컴포넌트 관점에서 children
prop은 <Details />
의 렌더링 출력 입니다.
RSC Quiz 2
이제 isOn
이 true
라고 가정해보겠습니다. note
를 수정하고 라우터에 경로를 "새로고침"하라고 지시했습니다.
그러면 이 라우트에 대한 RSC 트리가 refetch되고 Note
서버 컴포넌트가 최신 DB 내용을 포함한 note
prop을 받습니다.
- (1) 토글 상태가 초기화됩니까?
- (2) 아니면
<Details />
에 새로운 콘텐츠가 표시됩니까?
RSC Quiz 2 답변:
(2) 클라이언트 상태는 refetch 시에 초기화 되지 않습니다. 이것은 일반적인 React처럼 작동합니다.
같은 위치에서 같은 내용이 렌더링된다면 업데이트하는 동안에 상태가 유지됩니다.
초기화할 이유가 없습니다. 하지만 새로운 데이터는 나타납니다. <Toggle />
은 새로운 children
을 받습니다.
RSC Quiz 3
조금 더 꼬아보겠습니다.
위 컴포넌트 모두 서버 컴포넌트입니다.
하지만 마우스를 드래그하면 변경되는 열 너비와 같은 상태를 <Layout />
에 추가하려고 합니다.
<Layout />
을 클라이언트 컴포넌트로 만들 수 있을까요? 만약 그렇다면 드래그 시에 어떻게 될까요?
- (1) 아니요, 허용되지 않습니다.
- (2) 드래그 시에 모두 refetch 합니다.
- (3) 드래그 시에 refetch가 없습니다.
RSC Quiz 3 답변:
(3) 드래그 시에 refetch가 없습니다. 일반적으로 라우터에 요청하지 않는 한 RSC 트리는 refetch되지 않습니다.
<Layout />
컴포넌트 관점에서 보면 다음과 같습니다.
left
와 right
prop은 <Sidebar />
와 <Content />
의 렌더링 출력 이므로 디스플레이가 유지됩니다.
어떻게 <Sidebar />
와 <Content />
를 렌더링할지 말지 알 수 있을까요? JSX가 열심히 호출하고 있는걸까요?
그렇지 않습니다. 단지 <Layout />
(클라이언트에서 실행되는)을 렌더링하려고 할 때 props를 serialize하기 때문입니다.
serialize하는 동안 JSX를 발견하면 이를 실행합니다.
마무리하며
Next.js App Router를 사용하면서 직간접적으로 RSC를 사용하고 있습니다.
하지만 정확하게 이해하지 못하고 있었는데, 이 글을 통해 RSC에 대해 조금 더 이해할 수 있었습니다.
위 질문에도 나왔지만 React는 점점 더 복잡해지고 이해하기 어려워지고 있습니다.
하지만 이러한 부분은 React를 사용하는 개발자들이 더 쉽게 개발할 수 있도록 도와주는 것이라고 생각합니다.
마지막으로, 이 글이 여러분의 프로젝트에 RSC를 도입하는 데 도움이 되길 바랍니다.
더 많은 개발자들이 성능과 사용자 경험을 개선하는 데 활용할 수 있기를 기대합니다.
reference
- https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023
- https://twitter.com/dan_abramov/status/1633574036767662080
마지막 업데이트
4/30/2023