Logo
duck blog | 기술 블로그
Published on

내가 면접 때마다 React Server Components(RSC)를 물어보는 이유

Authors

기술 면접관으로 참여할 때마다 이력서에 Next.js나 최신 React를 써봤다고 적힌 지원자분들에게 거의 빼놓지 않고 던지는 질문이 있습니다.

“이번 프로젝트에 RSC(React Server Components)를 도입하셨다고 적어주셨네요. 왜 RSC를 쓰셨나요?”

그러면 열에 아홉은 거의 비슷한 답변을 들려주십니다.

지원자: “서버에서 렌더링을 처리해서 번들 사이즈를 줄이고, 첫 페이지 로딩 속도와 SEO를 개선하려고 도입했습니다.”

사실 이 답변을 들으면 고개를 끄덕이면서도 속으로는 조금 아쉽다는 생각이 먼저 듭니다. 틀린 말은 아니지만, 이 정도의 설명은 기술 블로그 첫 페이지나 튜토리얼 요약본만 읽어도 누구나 외워서 말할 수 있는 수준이기 때문입니다. 그래서 저는 보통 여기서 바로 꼬리질문을 던집니다.

면접관: “말씀해주신 장점들은 과거 Next.js Pages Router 시절의 SSR(getServerSideProps)로도 충분히 얻을 수 있던 것들 아닌가요? 지원자분이 생각하시는 RSC와 기존 SSR의 결정적인 차이는 무엇인가요?”

이 질문을 던졌을 때 명확하게 자기 생각과 아키텍처의 배경을 풀어내는 분은 생각보다 드뭅니다. 대부분은 당황하며 'use client'의 위치나 page.tsx 파일 작성법 같은 문법적 차이로 화제를 돌리곤 하더군요.

그리고 여기서 후보자의 깊이를 완전히 확인해보고 싶을 때, 저는 한 걸음 더 들어가 네트워크와 렌더링 파이프라인의 심화 질문을 던져봅니다.

면접관: “그렇다면 RSC가 서버에서 렌더링되어 브라우저로 내려올 때, 서버는 이걸 구체적으로 어떤 형태로 완성해서 내려보내고 브라우저는 이걸 어떻게 캐치해서 화면과 기존 상태를 유지하며 꿰어 맞추나요? 그리고 이 과정이 왜 네트워크 레벨에서 HTTP/2.0과 떼려야 뗄 수 없는 관계일까요?”

이 질문까지 도달하면 면접실의 공기가 사뭇 진지해집니다. 하지만 이 질문에 막힘없이 자신만의 언어로 원리를 설명하는 개발자를 만났을 때의 지적 희열은 이루 말할 수 없습니다.

제가 면접 자리에서 굳이 이 질문을 파고드는 이유는 지원자를 곤란하게 만들기 위해서가 아닙니다. RSC라는 기술 하나만 제대로 파고들어도, 이 개발자가 브라우저의 물리적 한계, 네트워크 프로토콜(HTTP/2.0), 그리고 React가 지난 10년 동안 고민해 온 렌더링 엔진의 진화 맥락 을 얼마나 깊이 있게 이해하고 있는지가 단번에 드러나기 때문입니다.


1. 기존 웹이 풀지 못했던 오랜 병목들

RSC가 왜 판도를 뒤집었는지를 이해하려면, 먼저 우리가 그동안 당연하게 감내해 왔던 기존 렌더링 방식들의 구조적 한계부터 짚고 넘어가야 합니다.

순수 CSR의 딜레마: 비대해지는 번들과 네트워크 폭포수

React의 부흥을 이끌었던 순수 CSR(Client-Side Rendering)은 사용자 인터랙션 면에서는 훌륭했지만, 두 가지 커다란 벽에 부딪혔습니다.

첫째는 끝없이 불어나는 자바스크립트 번들 입니다. 화면을 그리기 위한 라이브러리와 비즈니스 로직이 많아질수록 브라우저가 다운로드하고 파싱해야 하는 스크립트가 기하급수적으로 늘어났습니다. 스마트폰 기기 사양이나 네트워크가 조금만 안 좋아도 첫 화면이 뜨기까지(TTI) 긴 시간 하얀 화면을 바라봐야 했죠.

둘째는 더 고질적인 문제였던 네트워크 폭포수(Waterfall) 현상입니다. 컴포넌트 중심 아키텍처에서는 부모 컴포넌트가 렌더링되고 마운트된 뒤에야 useEffect 안에서 데이터를 패칭합니다. 그 데이터가 도착해야 비로소 자식 컴포넌트가 렌더링되고, 그 자식 컴포넌트도 마운트된 뒤에야 또 다른 API를 호출합니다.

// 순수 CSR에서 흔히 겪는 컴포넌트 레벨 워터폴
function PostDetail({ postId }) {
  const { data: post, isLoading } = usePost(postId);

  if (isLoading) return <Spinner />;

  // Post가 도착해야만 비로소 CommentList가 마운트되고, 내부에서 또 fetch를 시작합니다.
  return (
    <div>
      <h1>{post.title}</h1>
      <CommentList postId={postId} />
    </div>
  );
}

브라우저와 서버 사이의 물리적 거리가 멀수록, 이 왕복 지연 시간(RTT, Round Trip Time)은 겹겹이 쌓여 사용자를 지치게 만들었습니다.

기존 SSR(Pages Router)의 딜레마: All-or-Nothing과 JS 이중 전송

그래서 등장한 것이 Next.js의 getServerSideProps로 대표되는 전통적인 SSR이었습니다. 서버에서 HTML을 미리 빚어서 내려주니 첫 화면 로딩과 SEO 문제는 해결되는 듯 보였습니다. 하지만 이 역시 명확한 한계가 존재했습니다.

  1. 전부 아니면 전무(All-or-Nothing): 페이지 안에 무거운 API 호출이 단 하나라도 있으면, 그 데이터가 서버에서 준비될 때까지 브라우저는 첫 바이트(TTFB)를 구경조차 할 수 없었습니다. 서버 렌더링이 페이지 단위로 묶여 있었기 때문입니다.
  2. Hydration을 위한 자바스크립트 이중 전송: 서버에서 이미 HTML을 다 그렸음에도 불구하고, 브라우저가 버튼 클릭 같은 이벤트 리스너를 결합(Hydration)하려면 서버 렌더링에 쓰였던 컴포넌트 자바스크립트 코드가 브라우저 번들에도 고스란히 포함되어 전송되어야 했습니다.

예를 들어 블로그 글 본문을 보여주기 위해 200KB짜리 마크다운 파서 라이브러리(marked, shiki 등)를 서버에서 썼다고 가정해 보겠습니다. 서버에서 이미 파싱을 끝내고 완성된 HTML을 내려주었음에도, 브라우저에서 하이드레이션을 수행해야 한다는 이유만으로 그 200KB짜리 라이브러리 번들이 브라우저로 또 날아갑니다.

HTML도 보내고, 그 HTML을 만들기 위해 썼던 무거운 자바스크립트 코드도 브라우저로 또 보내는 낭비. 기존 SSR 구조에서는 이 굴레를 결코 벗어날 수 없었습니다.


2. React의 역사와 필연적 만남: 클라이언트 동시성의 ‘제로섬’ 한계

이 지점에서 시야를 잠깐 React 자체의 역사로 넓혀볼 필요가 있습니다. 예전에 정리했던 React 타임라인 글에서도 다루었듯, React 팀은 2017년부터 프레임워크 내부 엔진을 바닥부터 다시 갈아엎는 대공사를 진행해 왔습니다.

  • React 16 (Fiber): 렌더링 계산(Render Phase)을 잘게 쪼개고, 필요하다면 언제든 중단(Pause)하거나 재개할 수 있는 엔진 기반을 다졌습니다.
  • React 18 (Suspense & Concurrency): 이 엔진을 토대로 useTransition이나 Suspense 같은 동시성 API를 열어주었습니다. 긴급한 사용자 인터랙션(타이핑, 클릭)을 위해 무거운 렌더링의 우선순위를 뒤로 미루는 기술이 현실화되었습니다.

동시성 모드는 정말 매력적이었습니다. 하지만 실무에서 React 18을 깊게 다뤄본 개발자들은 곧 클라이언트 환경이 가진 근본적인 물리적 한계 를 마주하게 되었습니다.

바로 첫 페이지 렌더링(Initial Load) 시점의 제로섬(Zero-sum) 딜레마 였습니다.

사용자가 페이지를 탐색하는 도중의 화면 전환이라면, useTransition을 활용해 무거운 렌더링을 백그라운드로 밀어두는 것이 아주 매끄럽게 동작합니다. 하지만 맨 처음 사용자가 우리 웹사이트 URL을 치고 들어왔을 때는 어떨까요?

브라우저는 페이지에 필요한 수 메가바이트의 자바스크립트 번들을 전부 다운로드해야 하고, 최상단 루트 컴포넌트부터 최하단 푸터 컴포넌트까지 모든 Render Phase를 한 번씩은 반드시 거쳐야만 화면을 그릴 수 있습니다.

렌더링 우선순위를 아무리 잘게 쪼개고 지연시키더라도, 결국 브라우저가 처리해야 하는 총 연산량은 단 1바이트도 줄어들지 않습니다.

클라이언트 기기 안에서 작업 순서를 이리저리 미루고 바꾸는 것은 한정된 파이를 나눠 먹는 제로섬 게임에 불과했던 것입니다. 저사양 모바일 기기에서는 여전히 메인 스레드가 벅차올랐고, 하이드레이션이 끝나기 전까지 인터랙션이 굳어버리는 TTI 지연은 클라이언트 튜닝만으로는 극복할 수 없는 벽이었습니다.

React 팀은 깨달았습니다. 클라이언트 렌더링의 연산 순서를 조율하는 것만으로는 한계가 왔고, 애초에 브라우저가 하지 않아도 될 일은 브라우저 번들에서 완전히 덜어내야 한다 는 사실을요.

그렇게 10년에 걸쳐 다듬어 온 Fiber 엔진(중단/재개)과 Suspense(비동기 제어), 그리고 점진적 스트리밍 기술을 하나로 엮어 서버라는 거대한 컴퓨터 위에 올려놓은 결과물이 바로 React Server Components(RSC) 였습니다.


3. RSC가 판도를 완전히 뒤집은 본질

그렇다면 RSC는 기존의 문제들을 어떻게 해결했을까요? 문법적인 디테일을 걷어내고 아키텍처 관점에서 보면 세 가지 거대한 변화가 일어났습니다.

1) 진정한 Zero Bundle Size의 실현

서버 컴포넌트는 오직 서버에서만 실행됩니다. 그리고 브라우저로는 컴포넌트 자바스크립트 코드가 아니라, 서버에서 실행된 결과 데이터만이 날아갑니다.

앞서 예로 들었던 200KB짜리 마크다운 파서 라이브러리를 생각해보겠습니다.

// app/posts/[id]/page.tsx (서버 컴포넌트)
import { marked } from 'marked'; // 200KB짜리 무거운 라이브러리
import sanitizeHtml from 'sanitize-html';
import { db } from '@/lib/db';

export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const post = await db.post.findUnique({ where: { id } });
  
  // 무거운 연산과 HTML 변환이 오직 서버 CPU에서만 일어납니다.
  const htmlContent = sanitizeHtml(marked.parse(post.content));

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
      <LikeButton postId={id} /> {/* 상호작용이 필요한 부분만 클라이언트 컴포넌트 */}
    </article>
  );
}

이 컴포넌트가 실행될 때, markedsanitize-html 같은 무거운 라이브러리 코드는 브라우저 번들에 단 1바이트도 포함되지 않습니다. 브라우저는 오직 가벼운 UI 결과 구조와 하단의 인터랙티브한 <LikeButton /> 자바스크립트 코드만을 내려받습니다.

이것이 기존 SSR과의 가장 결정적인 차이 중 하나입니다. 기존 SSR은 브라우저 하이드레이션을 위해 서버 렌더링에 사용된 모든 컴포넌트 코드를 브라우저에도 번들링해 보냈지만, RSC는 서버 전용 컴포넌트의 번들 비용을 완벽하게 ‘0’으로 만듭니다.

2) 데이터 소스와의 물리적 거리 단축과 워터폴의 해소

컴포넌트가 브라우저가 아닌 백엔드 인프라 내부에서 실행된다는 것은, 데이터베이스나 내부 마이크로서비스와의 네트워크 레이턴시가 수 밀리초(ms) 단위로 줄어든다는 뜻 입니다.

브라우저에서 API를 여러 번 찌를 때는 서울과 미국 서부 서버를 오가며 수백 ms의 RTT가 겹겹이 쌓이지만, 서버 컴포넌트는 동일한 VPC나 데이터센터 내부 초고속망 안에서 데이터를 쿼리합니다.

게다가 컴포넌트 자체가 async/await 함수로 동작하므로, 지저분한 useEffect나 로딩 상태 플래그 없이도 직관적으로 데이터를 가져와 트리를 구성할 수 있습니다.

3) 페이지 단위가 아닌 컴포넌트 단위의 하이브리드 합성

과거에는 페이지 단위로 “이 페이지는 SSR”, “저 페이지는 CSR”처럼 거칠게 양자택일을 해야 했습니다.

반면 RSC 아키텍처에서는 한 화면 안에서 서버 컴포넌트와 클라이언트 컴포넌트가 레고 블록처럼 유기적으로 결합합니다.

// Server Component가 Client Component를 감싸고,
// Client Component의 children으로 또 다른 Server Component를 주입할 수 있습니다.
<DashboardLayout> {/* Server Component */}
  <InteractiveSidebar> {/* Client Component: 접고 펴는 상태 관리 */}
    <ServerUserProfile /> {/* Server Component: DB에서 유저 정보 직접 패칭 */}
  </InteractiveSidebar>
  <MainContent /> {/* Server Component */}
</DashboardLayout>

클라이언트 컴포넌트는 오직 사용자의 실시간 인터랙션(상태, 이벤트 리스너, 브라우저 API)이 필요한 리프(Leaf) 노드 수준으로 격리되고, 그 외의 뼈대와 무거운 데이터 처리는 서버 컴포넌트가 도맡는 이상적인 역할 분담이 완성되었습니다.


4. 한 걸음 더 깊게: 서버는 어떻게 완성하고, 브라우저는 어떻게 캐치할까?

서버의 RSC Payload 직렬화 및 브라우저 하이브리드 트리 결합
서버의 RSC Payload 직렬화 및 브라우저 하이브리드 트리 결합

면접에서 많은 분들이 가장 헷갈려하는 대목이 바로 여기입니다.

“서버에서 컴포넌트를 실행한다는 건 알겠는데, 그럼 서버는 대체 무엇을 브라우저에 보내고, 브라우저는 그걸 어떻게 받아서 화면에 끼워 맞추는가?”

많은 분들이 단순히 “서버에서 HTML을 만들어서 보낸다”거나, “그냥 JSON 객체를 보내는 것 아니냐”고 어림짐작하곤 합니다. 하지만 둘 다 아닙니다.

만약 서버가 단순 HTML 을 보낸다면, 페이지 내비게이션 시 브라우저에 이미 떠 있는 클라이언트 컴포넌트들의 내부 상태(입력 폼 내용, 스크롤 위치, 모달 열림 여부 등)가 전부 날아가 버립니다. 반대로 단순 JSON 데이터만 보낸다면, 컴포넌트의 계층 구조나 Suspense의 비동기 슬롯, 클라이언트 컴포넌트의 위치를 표현할 방법이 없습니다.

서버가 완성하는 것: RSC Payload (Wire Format)

서버는 컴포넌트 트리를 실행하면서 HTML도, 순수 JSON도 아닌 RSC Payload(와이어 포맷 스트림) 라는 특수한 직렬화 포맷을 만들어냅니다.

서버 컴포넌트를 만나면 태그와 props를 가상 돔 데이터로 변환하고, 클라이언트 컴포넌트를 만나면 자바스크립트 코드를 실행하는 대신 “이 위치에는 클라이언트 번들 파일(청크)의 특정 모듈을 끼워 넣어라”는 참조 포인터(Module Reference) 만을 직렬화합니다.

실제 네트워크 탭을 열어보면 대략 다음과 같은 형태의 텍스트 스트림이 한 줄씩 흘러내려오는 것을 볼 수 있습니다.

// 서버에서 브라우저로 흘러오는 RSC Payload의 개념적 형태
M1:{"id":"./src/components/LikeButton.tsx","name":"LikeButton","chunks":["client-chunk.js"]}
J0:["$","article",null,{"children":[
  ["$","h1",null,{"children":"RSC의 깊은 원리"}],
  ["$","div",null,{"dangerouslySetInnerHTML":{"__html":"<p>본문 내용...</p>"}}],
  ["$","$L1",null,{"postId":"123"}] // $L1: 위 M1 클라이언트 컴포넌트 참조!
]}]
  • M1: 클라이언트 컴포넌트의 모듈 정보(청크 파일 경로, export 이름)를 정의한 메타데이터입니다.
  • J0: 서버에서 실행된 가상 돔 트리입니다. 주목할 점은 <LikeButton /> 자리에 실제 코드가 들어간 게 아니라, 앞서 선언한 $L1이라는 참조 심볼이 자리 잡고 있다는 것입니다.

브라우저는 이걸 어떻게 캐치하고 꿰어 맞추는가?

브라우저의 React 런타임은 이 RSC Payload를 HTTP 스트림(ReadableStream)을 통해 청크 단위로 한 줄씩 실시간 수신합니다.

  1. 스트림 파싱과 슬롯 매핑: 브라우저는 날아오는 텍스트를 파싱하여 가상 돔 트리를 재구성합니다. 도중에 $L1 같은 클라이언트 컴포넌트 참조를 만나면, 해당 클라이언트 번들(client-chunk.js)을 즉시 백그라운드에서 로드하거나 이미 캐시된 모듈을 꺼내옵니다.
  2. 상태(State) 보존과 Fiber Reconciliation: 브라우저의 React 엔진은 이 서버 결과물을 바탕으로 기존 클라이언트의 Fiber 트리를 업데이트합니다. 이때 가장 중요한 점은, 클라이언트 컴포넌트가 가지고 있던 기존 State를 단 1도 초기화하지 않고 그대로 유지한 채, 서버 컴포넌트 슬롯만 마법처럼 교체(Reconciliation) 한다는 것입니다.
  3. Suspense 스트리밍의 결합: 만약 서버 컴포넌트 중 일부가 느린 DB 쿼리를 기다리고 있다면? 서버는 준비된 껍데기(Fallback UI)부터 먼저 클라이언트로 쏘아 보내고, 느린 데이터가 준비되는 즉시 뒤이어 해당 컴포넌트의 UI를 스트림 끝자락에 추가로 흘려보냅니다. 브라우저는 이를 받아 Suspense 자리에 부드럽게 끼워 넣습니다.

5. 네트워크의 퍼즐: 왜 HTTP/2.0 없이는 RSC도 없었을까?

제가 면접에서 후보자들에게 종종 던지는 가장 재미있는 꼬리질문이 바로 “RSC와 HTTP/2.0의 상관관계” 입니다.

예전에 다루었던 HTTP/2 핵심 기술 정리 글에서도 강조했듯, 웹의 병목은 언제나 네트워크 프로토콜의 한계와 맞닿아 있었습니다.

만약 우리가 10년 전 HTTP/1.1 시절에 지금의 RSC 아키텍처를 시도했다면 어떤 일이 벌어졌을까요? 단언컨대 엄청난 재앙이었을 것입니다.

HTTP/1.1 시대의 악몽: HOL 블로킹과 커넥션 고갈

HTTP/1.1의 가장 치명적인 약점은 하나의 TCP 연결에서 한 번에 하나의 요청/응답만 순차적으로 처리할 수 있다는 점이었습니다(Head-of-Line Blocking). 브라우저는 이를 극복하기 위해 도메인당 최대 6개의 TCP 커넥션만을 허용했습니다.

그런데 RSC의 핵심은 “서버 컴포넌트가 연산되는 대로 끊김 없이 청크를 스트리밍(Chunked Transfer)하는 것” 입니다.

만약 HTTP/1.1 환경에서 서버가 무거운 DB 쿼리를 수행하며 RSC Payload 스트림을 길게 붙잡고 있다면, 그 귀중한 6개 커넥션 중 하나가 끝날 때까지 완전히 묶여버립니다. 그 뒤에서 기다리고 있는 클라이언트 컴포넌트 JS 번들, 스타일시트(CSS), 폰트, 이미지들이 줄줄이 대기열(Queue)에 갇혀버리는 끔찍한 병목이 일어날 수밖에 없었죠.

[HTTP/1.1의 비극]
TCP 연결 1: [--- 긴 RSC Payload 스트리밍 진행 중 (점유) ---]
TCP 연결 2: [ critical.css ] ➔ 완료
TCP 연결 3: [ client-chunk.js (대기 중...) ] ❌ HOL 블로킹 발생!

HTTP/2.0 멀티플렉싱(Multiplexing)이 완성한 구원

HTTP/2.0 멀티플렉싱 파이프라인과 RSC 병렬 스트리밍
HTTP/2.0 멀티플렉싱 파이프라인과 RSC 병렬 스트리밍

RSC가 실무에서 부드럽게 날아다닐 수 있는 진짜 이유는, 인프라와 브라우저 생태계에 HTTP/2.0의 멀티플렉싱(Multiplexing) 이 완벽하게 안착했기 때문입니다.

HTTP/2.0은 단 하나의 TCP 연결 안에서 수백 개의 독립적인 양방향 스트림(Stream) 을 바이너리 프레임 단위로 쪼개어 동시에 병렬로 주고받습니다.

[HTTP/2.0 멀티플렉싱]
단일 TCP 커넥션
┌────────────────────────────────────────────────────────┐
│ [스트림 1: RSC Payload 프레임] [스트림 2: CSS 프레임]   │ ➔ 프레임 단위로
│ [스트림 3: JS 청크 번들 프레임] [스트림 1: RSC Payload] │    동시 전송!
└────────────────────────────────────────────────────────┘

이 덕분에 다음과 같은 환상적인 동시 작업이 가능해집니다:

  • 스트림 1: 서버가 데이터베이스를 조회하며 느긋하게 RSC Payload를 청크 단위로 스트리밍합니다.
  • 스트림 2, 3: 그 스트리밍 연결을 막지 않고, 클라이언트 컴포넌트에 필요한 수많은 작은 자바스크립트 청크 번들과 CSS가 동일한 연결 안에서 병렬로 쏟아져 들어옵니다.
  • 브라우저는 어떠한 블로킹도 없이, 날아오는 CSS와 JS를 파싱하면서 동시에 스트리밍으로 흘러오는 서버 가상 돔 트리를 꿰어 맞춥니다.

결국 “RSC의 점진적 스트리밍 렌더링”은 React 코어 팀의 소프트웨어적 고민과, HTTP/2.0이라는 네트워크 인프라의 진화가 가장 완벽한 타이밍에 맞아떨어져 탄생한 합작품 인 셈입니다.


6. 초기 과도기의 진통을 넘어, 왜 지금 ‘꽃을 피우고 있다’고 말하는가

사실 Next.js 13에서 App Router와 RSC가 처음 공개되었던 2022~2023년 무렵만 해도, 실무에서 이를 도입하는 것은 꽤나 고통스러운 모험이었습니다.

당시를 회고해보면 많은 개발자들이 혼란을 겪었습니다.

  • 과도하게 공격적이었던 기본 캐싱: fetch 요청마다 기본값으로 영구 캐시가 걸리는 등 디버깅을 어렵게 만드는 마법 같은 동작들이 많았습니다.
  • 서드파티 생태계의 부재: 인기 있는 UI 라이브러리나 차트 라이브러리들이 'use client' 지시어를 지원하지 않아 일일이 래퍼 컴포넌트를 만들어 감싸야 했습니다.
  • 하이드레이션 에러와 멘탈 모델 충돌: 서버 컴포넌트에서 클라이언트 컴포넌트로 함수를 props로 넘길 수 없다는 직렬화 규칙이나, 서버와 클라이언트 경계선에 대한 멘탈 모델이 정립되지 않아 많은 팀들이 삽질을 거듭했습니다.

하지만 지금은 상황이 완전히 달라졌습니다.

Next.js 15와 16을 거치며 논란이 되었던 기본 캐싱 정책이 개발자의 상식에 부합하도록(no-store 기본화 및 명시적 캐싱) 깔끔하게 정돈되었습니다. 주요 라이브러리 생태계 역시 RSC 호환을 기본적으로 마치며 성숙해졌습니다.

여기에 더해 최근 React 19와 맞물려 도입된 React Compiler의 자동 메모이제이션, 그리고 비동기 데이터 갱신을 우아하게 처리하는 Server Actionsuse API 가 퍼즐처럼 딱 맞아떨어졌습니다.

초기의 불완전했던 실험 단계를 완전히 통과해, 이제는 프로덕션 환경에서도 의심의 여지 없이 믿고 쓸 수 있는 현업의 새로운 기본값(Default) 으로 굳건히 안착한 것입니다. 10년 넘게 축적된 React의 렌더링 철학이 마침내 가장 안정적이고 세련된 형태로 만개한 시점에 우리가 서 있습니다.


7. 마치며: 면접관의 시선에서

다시 처음의 면접 이야기로 돌아와 봅니다.

만약 면접 자리에서 누군가 저에게 “그렇다면 당신은 면접자에게 어떤 답변을 기대하느냐?”고 묻는다면, 저는 이렇게 대답할 것 같습니다.

“저는 문법이나 프레임워크 사용법을 읊는 사람보다, 기술이 왜 그 방향으로 움직여야만 했는지 맥락을 이해하는 사람 과 일하고 싶습니다.”

RSC는 단순히 Vercel이나 React 팀이 심심해서 만든 기능이 아닙니다.

싱글 스레드 브라우저의 한계를 넘기 위해 Fiber를 만들고, 렌더링 우선순위를 조율하기 위해 Suspense를 만들었지만, 그럼에도 불구하고 클라이언트만으로는 해결할 수 없었던 ‘물리적인 연산 제로섬’의 한계를 돌파하기 위한 필연적인 고민의 종착역이었습니다.

그리고 그 고민은 브라우저 너머 RSC Payload의 직렬화 파이프라인, 그리고 단일 연결에서 병렬 스트림을 쏟아내는 HTTP/2.0 멀티플렉싱 인프라 와 맞물리며 마침내 완성되었습니다.

면접에서 RSC를 질문했을 때, 단순히 번들 크기나 SEO를 말하는 것에 그치지 않고 이런 흐름을 짚어주는 지원자를 만난다면 저는 무척 반가울 것 같습니다.

“React 18 동시성으로 렌더링 우선순위를 미뤄도 첫 로딩의 전체 연산량은 줄지 않더군요. 결국 브라우저 번들 자체를 비워내야 한다는 제로섬 한계를 부수기 위해 RSC가 나왔습니다. 서버는 가상 돔과 클라이언트 모듈 참조 포인터를 담은 RSC Payload를 쏘아주고 브라우저는 상태 손실 없이 이를 결합하죠. 특히 이 스트리밍 과정이 HTTP/2.0의 멀티플렉싱 덕분에 다른 번들을 막지 않고 병렬로 처리되는 점이 제 프로젝트의 네트워크 병목을 획기적으로 풀어주었습니다.”

이런 대답이 나온다면, 더 이상 기술 면접이 아니라 즐거운 동료 개발자와의 아키텍처 토론이 시작되는 순간이 될 테니까요.

글을 맺으며 문득 드는 생각인데, 이렇게 제 면접 노트를 블로그에 전부 공개해도 괜찮을까 싶기도 합니다.

하지만 솔직히 제 블로그는 제가 링크를 직접 던져주지 않는 이상 굳이 찾아오시는 분이 많지 않아서 큰 걱정은 안 됩니다. 게다가 설령 이 글이 어딘가에 돌아다닌다 해도, 단순히 활자를 읽고 외우는 것과 본인의 프로젝트 경험 속에서 아키텍처의 트레이드오프를 체화해 자기 언어로 풀어내는 건 완전히 다른 영역이니까요.

아마 이렇게 노트를 다 풀어놔도, 막상 제 면접 자리에 오셔서 이 질문에 속 시원히 답을 잘하시는 분은 15%도 안 될 것 같습니다. 허허.

댓글

새 글 알림 받기

실무에서 바로 써먹을 수 있는 개발 팁과 경험담을 받아보세요

#실무 개발 경험담#최신 기술 트렌드#성능 최적화 노하우#개발 팁과 인사이트

개인정보는 뉴스레터 발송 목적으로만 사용되며, 언제든 구독을 해지할 수 있습니다.