- Mux는 mux.com과 docs.mux.com을 React Server Components로 옮기며, 서버와 클라이언트 실행 경계를 나누는 일이 번들 크기와 hydration 비용에 직접 영향을 준다는 점을 확인함
- RSC는 컴포넌트가 서버에서 직접 데이터를 가져오고 결과를 스트리밍할 수 있게 해, 느린 데이터 호출이 있어도 화면 일부를 먼저 보여줄 수 있음
- 실제 마이그레이션의 가장 큰 걸림돌은 CSS-in-JS 미지원, Server Components의 React Context 제약, 서버/클라이언트 경계를 계속 추적해야 하는 복잡성이었음
- Next.js 13의 app directory에서는 기본이 Server Component이며, 루트의
use client를 점차 아래로 내리는 방식으로 점진적 도입이 가능함 - Suspense,
loading.js, 서버 전용 라이브러리 유지,server-only같은 패턴은 성능 이득이 필요한 곳에만 신중히 적용해야 하며, 팀의 인지 비용도 함께 계산해야 함
Mux가 RSC로 옮긴 범위
- Mux는 문서 사이트 재구성과 브랜드 변경을 진행하면서 mux.com과 docs.mux.com을 Server Components로 이동함
- React Server Components는 실제 코드베이스에서도 적용 가능하고 쓸 가치가 있을 수 있지만, 제약과 복잡성도 함께 따라옴
- 이 경험은 RSC가 왜 필요한지, 어디에 적합한지, 어떤 상황에서 까다로운지, 실제 코드베이스에 어떻게 점진적으로 들여올 수 있는지를 중심으로 정리됨
CSR, SSR/SSG 이후 RSC가 겨냥한 문제
- 초기 서버 렌더링 방식은 PHP 같은 기술로 서버에서 데이터를 가져오고 무거운 CPU 작업을 처리한 뒤, 클라이언트에는 가벼운 HTML을 전달했음
- CSR/SPA는 렌더링 코드를 JavaScript로 클라이언트에 보내 상호작용을 빠르게 처리할 수 있게 했지만, 검색 엔진이 JavaScript를 실행하지 않거나 서버에 비밀값을 유지해야 하거나 저성능 기기·느린 연결을 만나는 상황에서는 약점이 드러남
- SSR/SSG는 Next.js와 Gatsby 같은 도구를 통해 서버에서 HTML과 JavaScript를 함께 생성해 클라이언트로 보내는 방식임
- 사용자는 HTML을 즉시 볼 수 있음
- JavaScript가 로드되면 사이트가 상호작용 가능해짐
- 검색 엔진도 HTML을 읽을 수 있음
- 기존 SSR/SSG에도 비용은 남아 있음
- 페이지 생성에 쓰인 JavaScript 대부분을 클라이언트로 보내고, 클라이언트가 다시 실행해 HTML과 결합하는 hydration이 필요함
- 서버 렌더링이 느린 데이터베이스 호출이나 많은 코드 실행 때문에 오래 걸리면 사용자가 기다려야 함
React Server Components가 바꾸는 지점
- React Server Components는 클라이언트가 아니라 서버에서 실행되는 React 컴포넌트임
- RSC 지원 프레임워크는 코드가 실행될 위치를 명시적으로 나눌 수 있게 함
- Server Components: 서버에서만 실행되어야 하는 코드
- Client Components: 클라이언트에서 실행되어야 하는 코드
- 실행 위치가 분리되면 클라이언트로 보내는 JavaScript가 줄고, hydration 중 수행해야 하는 작업도 줄어듦
- Server Component는 컴포넌트 내부에서 직접 데이터를 가져올 수 있음
- Node 라이브러리나
fetch를 사용할 수 있음 - 페이지 수준에서
getServerSideProps로 데이터를 한꺼번에 가져와 props로 계속 내려보내는 방식을 줄일 수 있음 useEffect로 복잡한 로딩 상태를 관리하는 경우도 줄어듦
- Node 라이브러리나
- 데이터 가져오기가 끝난 Server Component는 결과를 클라이언트로 스트리밍할 수 있음
- 느린 컴포넌트를 기다리는 동안 나머지 사이트를 먼저 보여줄 수 있음
- 클라이언트 사용자 동작에 대응해 서버에서 데이터를 가져오고 응답을 스트리밍하는 방식도 가능하지만, 이는 엄밀히는 RSC가 아니라 React Actions에 해당함
RSC가 까다로운 부분
- CSS-in-JS는 현재 Server Components에서 동작하지 않음
- Mux의 RSC 전환에서는 styled-components에서 Tailwind CSS로 옮기는 작업이 가장 큰 비중을 차지함
- CSS-in-JS에 크게 의존한 코드베이스라면 별도 마이그레이션 작업이 필요함
- React Context는 Client Components에서만 접근할 수 있음
- Server Components 사이에서 props 없이 데이터를 공유하려면 일반 모듈을 사용해야 할 가능성이 큼
- React 애플리케이션의 특정 하위 트리에만 데이터를 제한하는 좋은 메커니즘은 Server Components에 없음
- Mux 문서 사이트에서는 Context 사용 영역이 상호작용이 많아 어차피 클라이언트로 보내야 했기 때문에 큰 문제가 되지 않았음
- 마케팅 사이트에서는 테마 공유가 문제가 됨
- pre-footer의 각 컴포넌트가 녹색 배경 위에 있다는 사실을 알아야 어두운 녹색 테두리를 쓸 수 있었음
- Context 대신 CSS custom properties를 적극 활용해 우회함
- RSC는 실행 위치와 데이터 가져오기 방식에 유연성을 주지만, 그만큼 복잡성도 늘어남
- 새 개발자는 “무엇이 서버에서 실행되고 무엇이 클라이언트에서 실행되는지”를 계속 확인해야 함
- PR마다 불필요하게 클라이언트로 보내진 코드에 대한 피드백이 발생함
- 개발 중 서버 로그인지 브라우저 로그인지 확인하려고
console.log를 넣는 일이 잦았음 - 캐싱도 별도의 복잡성을 더함
Next.js 13에서 RSC를 쓰는 기본 방식
- 작성 시점 기준 프로덕션 준비가 된 RSC 구현은 Next.js 13의 app directory임
- Next.js 13 app directory에서는 기본적으로 작성하는 컴포넌트가 Server Component임
- 기본 상태에서는 페이지 코드가 클라이언트로 보내지지 않음
- 클라이언트에는 HTML만 전달됨
- Server Component에
async를 붙이면 컴포넌트 내부에서 데이터를 가져올 수 있음 - 느린 데이터 가져오기가 있는 Server Component는
React.Suspense로 감쌀 수 있음- 클라이언트에는 fallback UI가 먼저 표시됨
- 서버가 데이터를 가져오고 렌더링을 마치면 결과 컴포넌트가 스트리밍됨
- Suspense boundary는 데이터 스트리밍뿐 아니라 사용자 상호작용에 따라 특정 영역의 hydration 우선순위를 조정하는 selective hydration에도 활용될 수 있음
- 클라이언트에서 실행되어야 하는 코드는 파일 상단에
"use client"를 추가함onClick리스너나useState처럼 클라이언트 상태와 상호작용이 필요한 컴포넌트에 사용함"use client"가 붙은 컴포넌트가 import하는 모든 컴포넌트도 클라이언트로 보내짐
- RSC를 지원하지 않는 라이브러리는 Client Component에서 import해 클라이언트 번들에 포함시킬 수 있음
- 예시는
@mux/mux-player-react를 감싸는ClientMuxPlayer컴포넌트임
- 예시는
Server Component와 Client Component 선택 기준
- Server Components는 클라이언트로 보낼 필요가 없는 코드에 적합함
- 블로그 글 본문 렌더링
- 코드 블록 구문 강조처럼 비용이 큰 작업
- 데이터 가져오기
- Client Components는 사용자의 입력에 반응하거나 시간이 지나며 상태가 바뀌는 UI에 적합함
useState- 이벤트 리스너
- 클라이언트 측 상호작용
- 전체 앱을 Client Components로 만들면 기존 SSR 프레임워크와 비슷하게 동작함
- 전체 앱을 한 번에 Server Components로 바꿀 필요는 없고, 이득이 큰 위치부터 점진적으로 도입할 수 있음
실제 코드베이스에 점진적으로 들여오는 3단계
- Mux가 사용한 플레이북은 세 단계임
- 앱 루트에
"use client"지시어를 추가함 - 지시어를 렌더링 트리에서 가능한 한 아래로 이동함
- 성능 문제가 드러날 때 고급 패턴을 적용함
- 앱 루트에
- 1단계에서는 Next.js 13의 최상위
page.tsx에"use client"를 추가해 기존처럼 동작하게 함 - 서버 측 데이터 가져오기가 필요하면 Server Component를 Client Component의 부모로 추가함
- Server Component가 데이터를 가져옴
- 가져온 데이터를 Client Component에 props로 전달함
- 기존
getServerSideProps역할을 대체할 수 있음
- 2단계에서는
"use client"를 최상위 컴포넌트에서 자식 컴포넌트로 옮김- 클라이언트 코드가 필요 없는
<Title />에서는 지시어를 제거해 순수 HTML로 보낼 수 있음 - 클라이언트 코드가 필요한
<Player />에서는 오류가 발생하므로"use client"를 유지함
- 클라이언트 코드가 필요 없는
- 이 방식은 새 컴포넌트와 기존 리팩터링 대상에서 Server Components를 고려하게 만들고, 번들 크기를 일부 줄이는 데 도움을 줌
성능 문제가 있을 때 적용한 패턴
- Mux 문서 사이트는 대부분 정적으로 생성되지만, changelog sidebar는 CMS에서 가져옴
- sidebar를 Suspense로 감싸면 CMS fetch가 끝날 때까지 앱 나머지 부분이 기다릴 필요가 없음
- Next.js 13의 loading.js 관례도 Suspense와 streaming을 내부적으로 사용함
- 큰 라이브러리를 서버에 남기려면 Client Components와 Server Components 배치를 조정해야 함
- 예시로 구문 강조 라이브러리 Prism을 서버에 유지함
Client Component 안에 Server Component를 섞는 법
- Client Component가 import한 컴포넌트는 함께 Client Component가 됨
- Server Component를 Client Component의 자식으로 두고 싶다면 import하지 말고
children이나 props로 전달해야 함- Server Component는 서버에서 렌더링됨
- 직렬화된 결과가 Client Component로 전달됨
- 잘못된 방식은 Client Component 파일에서 Server Component를 직접 import하는 것임
- 올바른 방식은 가장 가까운 상위 Server Component로 올라가서 Client Component에 Server Component를 자식이나 prop으로 넘기는 것임
한 파일을 서버/클라이언트 반반으로 나눌 수는 없음
- 한 파일의 절반은 Server Component, 절반은 Client Component로 만드는 방식은 불가능함
- Mux는 기능을 두 파일로 나누는 패턴을 자주 사용함
CodeBlock.server.js: 큰 구문 강조 라이브러리를 import하고 서버에서 렌더링함CodeBlock.client.js:useState와onClick을 사용해 사용자가 코드 예제를 전환할 수 있게 함
- 서버에서 렌더링한 예제들은 props로 Client Component에 전달되므로 서버 전용 작업이 클라이언트 번들로 넘어가지 않음
index.js에서CodeBlock.server.js를 다시 export하면 사용자는CodeBlock만 import해도 내부 서버/클라이언트 분리를 의식하지 않아도 됨
서버에서만 실행되게 보장하는 방법
- 초기에는 개발 중
console.log를 추가해 로그가 서버에서 나오는지 브라우저에서 나오는지 확인함 - 서버 전용 코드가 번들에 포함되지 않도록 보장하려면 server-only package를 import할 수 있음
server-only는 큰 라이브러리나 비밀 키가 잘못된 위치로 이동하지 않게 하는 데 유용함- Next.js는 환경 변수가 실수로 브라우저 번들에 포함되는 것을 막는 보호 장치를 제공함
- 파일 상단의
server-only는 유지보수에도 도움을 줌- 관리자가 해당 파일이 서버에서 실행된다는 사실을 바로 알 수 있음
도입 여부를 판단할 때의 비용과 이득
- React Server Components는 무료로 얻는 기능이 아님
- 비용에는 CSS-in-JS와 React Context 제약뿐 아니라 다음이 포함됨
- 서버와 클라이언트 실행 위치 이해
- hydration 이해
- 인프라 비용
- Client Components와 Server Components 혼합으로 인한 코드 복잡성
- 복잡성은 버그가 들어오고 코드 유지보수성이 낮아질 수 있는 표면을 늘림
- 프레임워크는 복잡성을 줄여주지만 제거하지는 않음
- 기대할 수 있는 이득은 다음과 같음
- 더 작은 번들 크기
- 더 빠른 실행
- SEO에 중요한 성능 개선
- 복잡하고 데이터가 많은 사이트를 위한 고급 데이터 로딩 패턴
- 팀이 추가 인지 비용을 감당할 준비가 되어 있고 성능 이득이 충분하다면 RSC가 적합할 수 있음