Netlify
리액트 라우터의 리액트 서버 컴포넌트 접근 방식
리액트 라우터의 RSC 는 점진적 마이그레이션 이 핵심. loader 에서 데이터가 아니라 JSX 를 반환하는 패턴이 타입과 번들 크기 모두에 이득이다.
핵심 요약
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(), /* ... */]
이후 세 가지 패턴 중 하나를 선택한다.
- Loader 에서 JSX 반환: 기존
loader는 유지하되,movies데이터 대신moviesUIJSX 를 반환 → 하이드레이션 데이터가 아니라 렌더 결과만 전송 - 전체 라우트를
ServerComponent로:default export대신ServerComponent함수를 export,loader자체가 사라짐 - 서버 함수 + 폼 액션:
'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[] 로 단순.
모든 페이지의 loader 에서 JSX 를 반환하는 것. 데이터가 계속 재사용 되거나 클라이언트 상호작용에 필요한 경우엔 기존 패턴이 더 낫다. RSC 가 '항상 작은 페이로드' 를 보장하진 않는다.
`'use client'` vs `'use server'` 가 말하는 건 '번들링 방향'
두 지시어는 컴파일러 힌트 다.
| 지시어 | 의미 | 번들링 결과 |
|---|---|---|
'use client' | 이 모듈의 export 는 브라우저에 필요 | 클라이언트 번들에 포함, 서버 렌더 시 경계 표시 |
'use server' | 이 모듈의 export 는 RPC 호출 대상 | 서버에만 남음, 클라이언트에는 함수 참조만 주입 |
서버 컴포넌트 안에서 클라이언트 컴포넌트를 그냥 import 하면 된다. 경계 관리는 번들러가 한다.
'use server' 모듈에 순수 유틸 함수를 섞어 두는 것. 해당 모듈의 모든 export 가 RPC 엔드포인트로 노출되어 공격 면이 늘어난다. RPC 대상 파일은 전용으로 분리한다.
증분 마이그레이션 = '경로별 독립 전환'
/admin/* 팀이 RSC 로 전환한다고 해서 /shop/* 팀이 영향을 받지 않는다. 중첩 구조에서 자식 라우트가 서버/클라이언트인 것과 부모의 모드는 서로 독립이다.
root (client)
├─ /shop (client, 기존 유지)
└─ /admin (server)
└─ /admin/orders (client, 필요시)
Next.js App Router 와 가장 큰 차이점으로, 대기업 프로덕션 앱의 점진 도입을 현실적으로 만든다.
'RSC 로 바꾸면 반드시 빠르다' 는 기대. 데이터+템플릿을 함께 보내는 게 작은 페이지에서는 오히려 더 무거울 수 있다. 실제 페이로드를 네트워크 탭에서 비교해야 한다.
읽는 순서
- 1이론
리액트 공식
Server Components문서와 리액트 라우터 RSC Experimental 문서에서use client/use server지시어의 의미, 그리고 '경계(boundary)' 개념을 정리한다. - 2구현
create-react-router프로젝트에unstable_reactRouterRSC를 켜고, 한 라우트만ServerComponent로 전환해 본다. 네트워크 탭에서 RSC 페이로드(?_rsc형태)와 JSON API 응답의 크기를 비교. - 3실무
현재 앱에서 'CMS 데이터로 UI 형태가 결정되는' 페이지가 있는지 확인하고, 그 한 페이지만 RSC loader 로 옮겨 보면 어떤 번들 변화가 있을지 가설을 세운다.
- 4설명
동료에게 'Next.js App Router 대신 리액트 라우터 RSC 를 고려해야 하는 조건' 을 3가지로 요약해 설명한다. 증분 마이그레이션·정적 배포·기존 라우트 유지 관점.
면접 연결 질문
[감점 답변] 'API 차이' 나열. [좋은 답변] 핵심은 점진적 마이그레이션 가능성. 리액트 라우터는 라우트 단위로 server/client 를 독립적으로 전환할 수 있고, 자식·부모가 서로 다른 모드라도 동작한다. 또한 런타임 서버 없이 정적 빌드만으로도 RSC 이점(페이로드 분리) 일부를 얻는다. Next.js 는 기본적으로 런타임 서버 전제.
[감점 답변] '빠르다'. [좋은 답변] JSON 데이터 전체를 클라이언트로 보낼 필요가 없어 번들 + RSC 페이로드 합계가 감소하고, 상호작용이 없는 컴포넌트는 하이드레이션도 생략된다. 특히 CMS 처럼 '데이터 모양으로 UI 결정' 하는 케이스에서 동적 임포트를 제거할 수 있다.
[감점 답변] '폼이 제출된다'. [좋은 답변] 번들러가 해당 함수를 RPC 엔드포인트 로 등록하고, 클라이언트에는 함수 참조 ID 만 남긴다. 폼 제출 시 리액트가 해당 ID 로 서버에 FormData 를 POST → 서버에서 원함수 실행 → 응답으로 UI 업데이트. 라우트 정의 없이 컴포넌트 단위 mutation 이 가능해진다.
자기 점검
loader 가 여전히 필요하다고 오해. 서버 컴포넌트는 async 함수라 컴포넌트 내부에서 await getMovies() 가 그대로 가능하다.
'use client' 를 서버 컴포넌트 파일에 같이 쓰면 된다는 착각. 'use client' 는 파일 맨 위에만 선언되며, 해당 파일 전체가 클라이언트 경계가 된다.