FEInterview Prep

Netlify

리액트 라우터의 리액트 서버 컴포넌트 접근 방식

리액트 라우터의 RSC 는 점진적 마이그레이션 이 핵심. loader 에서 데이터가 아니라 JSX 를 반환하는 패턴이 타입과 번들 크기 모두에 이득이다.

2025-12-30·7분 읽기
React아키텍처빌드/도구
원문 보기 ↗

핵심 요약

RSC 활성화는 vite.config.ts 에서 플러그인 두 개를 교체하는 것으로 끝난다.

// 기존
import { reactRouter } from '@react-router/dev/vite';
// RSC
import { unstable_reactRouterRSC as reactRouterRSC } from '@react-router/dev/vite';
import rsc from '@vitejs/plugin-rsc';

plugins: [reactRouterRSC(), rsc(), /* ... */]

이후 세 가지 패턴 중 하나를 선택한다.

  1. Loader 에서 JSX 반환: 기존 loader 는 유지하되, movies 데이터 대신 moviesUI JSX 를 반환 → 하이드레이션 데이터가 아니라 렌더 결과만 전송
  2. 전체 라우트를 ServerComponent: default export 대신 ServerComponent 함수를 export, loader 자체가 사라짐
  3. 서버 함수 + 폼 액션: 'use server' 지시어로 RPC 엔드포인트 자동 생성, 컴포넌트에 직접 <form action={fn}> 바인딩 가능 → 라우트가 아니라 컴포넌트 단위 mutation

리액트 라우터의 RSC 는 'Next.js 처럼 앱 전체를 서버 컴포넌트로 전환' 이 아니다. 라우트마다 서버/클라이언트 모드를 독립적으로 선택할 수 있는 트리 단위 점진 마이그레이션 모델이다. 중첩된 자식 라우트가 서버여도 부모는 클라이언트, 그 반대도 가능하다.

Next.js App Router 의 RSC 는 올인(all-in) 이다. 한 페이지라도 서버 컴포넌트로 전환하려면 빌더/파일 시스템 전체가 바뀐다. 리액트 라우터는 다른 길을 고른다.

  • 기존 클라이언트 라우트는 건드리지 않고 새 라우트만 서버로
  • 서버 라우트 안에서 'use client' 경계는 그냥 동작
  • 빌드 서버만으로도 배포 가능 → 런타임 서버 불필요

대규모 앱에서 팀 단위로 영역을 나눠 들어가기에 적합하다.

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

`loader` 가 데이터 대신 UI 를 반환한다는 의미

전통 방식은 JSON → 클라이언트 하이드레이션 → 렌더. CMS 콘텐츠처럼 데이터 모양을 보기 전엔 어떤 컴포넌트를 쓸지 알 수 없는 경우, 클라이언트로 스키마를 다 보내고 동적 임포트로 컴포넌트를 고르면 번들이 비대해진다.

// RSC loader — 데이터는 서버에만, UI 만 넘긴다
export async function loader() {
  const movies = await getMovies();
  return {
    moviesUI: movies.map(m => <MovieCard key={m.id} movie={m} />),
  };
}

결과: 페이로드 크기 감소 + 타입이 loaderData.moviesUI: JSX.Element[] 로 단순.

loaderServerComponentRSC payloadSuspense
자주 하는 오해

모든 페이지의 loader 에서 JSX 를 반환하는 것. 데이터가 계속 재사용 되거나 클라이언트 상호작용에 필요한 경우엔 기존 패턴이 더 낫다. RSC 가 '항상 작은 페이로드' 를 보장하진 않는다.

`'use client'` vs `'use server'` 가 말하는 건 '번들링 방향'

두 지시어는 컴파일러 힌트 다.

지시어의미번들링 결과
'use client'이 모듈의 export 는 브라우저에 필요클라이언트 번들에 포함, 서버 렌더 시 경계 표시
'use server'이 모듈의 export 는 RPC 호출 대상서버에만 남음, 클라이언트에는 함수 참조만 주입

서버 컴포넌트 안에서 클라이언트 컴포넌트를 그냥 import 하면 된다. 경계 관리는 번들러가 한다.

use clientuse serverform actionRPC
자주 하는 오해

'use server' 모듈에 순수 유틸 함수를 섞어 두는 것. 해당 모듈의 모든 export 가 RPC 엔드포인트로 노출되어 공격 면이 늘어난다. RPC 대상 파일은 전용으로 분리한다.

증분 마이그레이션 = '경로별 독립 전환'

/admin/* 팀이 RSC 로 전환한다고 해서 /shop/* 팀이 영향을 받지 않는다. 중첩 구조에서 자식 라우트가 서버/클라이언트인 것과 부모의 모드는 서로 독립이다.

root (client)
 ├─ /shop        (client, 기존 유지)
 └─ /admin       (server)
     └─ /admin/orders  (client, 필요시)

Next.js App Router 와 가장 큰 차이점으로, 대기업 프로덕션 앱의 점진 도입을 현실적으로 만든다.

incrementalroute boundarynested routes
자주 하는 오해

'RSC 로 바꾸면 반드시 빠르다' 는 기대. 데이터+템플릿을 함께 보내는 게 작은 페이지에서는 오히려 더 무거울 수 있다. 실제 페이로드를 네트워크 탭에서 비교해야 한다.

읽는 순서

  1. 1이론

    리액트 공식 Server Components 문서와 리액트 라우터 RSC Experimental 문서에서 use client/use server 지시어의 의미, 그리고 '경계(boundary)' 개념을 정리한다.

  2. 2구현

    create-react-router 프로젝트에 unstable_reactRouterRSC 를 켜고, 한 라우트만 ServerComponent 로 전환해 본다. 네트워크 탭에서 RSC 페이로드(?_rsc 형태)와 JSON API 응답의 크기를 비교.

  3. 3실무

    현재 앱에서 'CMS 데이터로 UI 형태가 결정되는' 페이지가 있는지 확인하고, 그 한 페이지만 RSC loader 로 옮겨 보면 어떤 번들 변화가 있을지 가설을 세운다.

  4. 4설명

    동료에게 'Next.js App Router 대신 리액트 라우터 RSC 를 고려해야 하는 조건' 을 3가지로 요약해 설명한다. 증분 마이그레이션·정적 배포·기존 라우트 유지 관점.

면접 연결 질문

hard리액트 라우터의 RSC 구현이 Next.js App Router 의 RSC 와 구조적으로 다른 점은?
힌트

[감점 답변] 'API 차이' 나열. [좋은 답변] 핵심은 점진적 마이그레이션 가능성. 리액트 라우터는 라우트 단위로 server/client 를 독립적으로 전환할 수 있고, 자식·부모가 서로 다른 모드라도 동작한다. 또한 런타임 서버 없이 정적 빌드만으로도 RSC 이점(페이로드 분리) 일부를 얻는다. Next.js 는 기본적으로 런타임 서버 전제.

medium`loader` 에서 데이터 대신 UI 를 반환하는 패턴이 하이드레이션 관점에서 얻는 이득은?
힌트

[감점 답변] '빠르다'. [좋은 답변] JSON 데이터 전체를 클라이언트로 보낼 필요가 없어 번들 + RSC 페이로드 합계가 감소하고, 상호작용이 없는 컴포넌트는 하이드레이션도 생략된다. 특히 CMS 처럼 '데이터 모양으로 UI 결정' 하는 케이스에서 동적 임포트를 제거할 수 있다.

medium`'use server'` 로 만든 서버 함수를 `<form action={fn}>` 에 바인딩할 때 내부적으로 무슨 일이 일어나는가?
힌트

[감점 답변] '폼이 제출된다'. [좋은 답변] 번들러가 해당 함수를 RPC 엔드포인트 로 등록하고, 클라이언트에는 함수 참조 ID 만 남긴다. 폼 제출 시 리액트가 해당 ID 로 서버에 FormData 를 POST → 서버에서 원함수 실행 → 응답으로 UI 업데이트. 라우트 정의 없이 컴포넌트 단위 mutation 이 가능해진다.

자기 점검

`ServerComponent` 를 export 하는 라우트에서 `loader` 가 사라지는 이유를 말해보세요.
async component데이터 직접 fetchloader 불필요
자주 하는 오해

loader 가 여전히 필요하다고 오해. 서버 컴포넌트는 async 함수라 컴포넌트 내부에서 await getMovies() 가 그대로 가능하다.

서버 컴포넌트 안에서 `useState` 를 쓰는 컴포넌트를 호출하려면 어떻게 해야 하는가?
use client경계번들 분리
자주 하는 오해

'use client' 를 서버 컴포넌트 파일에 같이 쓰면 된다는 착각. 'use client'파일 맨 위에만 선언되며, 해당 파일 전체가 클라이언트 경계가 된다.