외부 원문 링크
프레임워크 없는 바닐라 리액트 서버 컴포넌트
Next.js 같은 프레임워크 없이 RSC 를 도입할 수 있게 해주는 Forket 의 동작 원리. 코드를 server/client 두 버전으로 분리하고 client boundary 를 직렬화하는 패턴을 보여 준다.
핵심 요약
Forket 의 흐름: ① AST 로 두 그래프(server/client) 빌드 → ② server 빌드는 <Comments> 같은 client 컴포넌트를 <template type="forket/start/..."> 마커로 감싸고 props 를 직렬화한 <script> 로 hydration 부트스트랩 → ③ client 빌드는 "use server" 함수를 window.FSA_call("$FSA_xxx") 호출로 치환하고 실제 구현은 번들에서 제거 → ④ "use client" 가 있는 진입점이 hydration 대상 컴포넌트를 전역 스코프로 노출. 결국 "server 컴포넌트는 HTML+JSON, client 컴포넌트는 hydration island" 라는 RSC 의 본질을 framework-agnostic 하게 구현.
RSC 는 마법이 아니라 "같은 소스를 server 빌드와 client 빌드 두 버전으로 분리하고, client boundary 의 props 를 JSON 직렬화로 전달한다" 는 단순한 패턴이다. Forket 은 그 패턴을 라이브러리에서 분리해 빌드 도구·런타임 글루로 추상화한 도구다.
면접·실무에서 "use client 가 정확히 뭘 하나" 또는 "왜 server 컴포넌트에서 client 컴포넌트로는 직렬화 가능한 props 만 보낼 수 있나" 를 정확히 답하려면 빌드 시 두 그래프, boundary 직렬화, Promise/Server Action 의 특수 처리라는 세 축을 알아야 한다. Next.js 의 추상에 갇혀 있으면 디버깅 못 한다.
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
RSC 의 본질은 "두 개의 빌드 + boundary 직렬화"
Forket 은 같은 소스 를 두 번 변환한다. server 빌드는 client 컴포넌트를 placeholder boundary 로 두고 hydration 부트스트랩 코드를 함께 출력, client 빌드는 server-only 코드를 RPC 호출로 치환. server-only 의 의존성은 client 번들에 절대 새지 않는다는 보장이 핵심.
RSC 를 "SSR 의 새 버전" 으로만 이해하는 것. SSR 은 HTML 만 보내지만 RSC 는 HTML + 직렬화된 트리 + client island 부트스트랩 을 함께 보낸다는 점이 다르다.
client boundary 에는 직렬화 가능한 값만 넘길 수 있는 "이유"
server 가 client 컴포넌트에 props 를 넘길 때 Forket 은 JSON.stringify(forketSerializeProps(...)) 로 직렬화한다. 함수는 "$FSA_*" ID 로, Promise 는 placeholder 문자열로 변환된 뒤 client 에서 실제 Promise 로 복원된다. 함수·클래스 인스턴스·DOM 참조는 직렬화 못 하니 props 로 못 보낸다는 제약은 여기서 나온다.
"왜 client 컴포넌트에 onClick 함수를 server 에서 못 넘기지?" 라는 질문을 "규약이라서" 로 답하기. 실제론 직렬화 표현이 없기 때문 이고, 우회는 Server Action ID 로 표현하는 것이다.
Server Action 은 client 빌드에서 RPC stub 으로 치환된다
"use server" 가 붙은 함수는 client 빌드에서 실제 코드가 사라지고 window.FSA_call("$FSA_createNote", "createNote")(...args) 처럼 서버에 HTTP 호출을 보내는 stub 으로 바뀐다. 즉 client 번들에는 DB 접근 코드가 들어가지 않는다.
Server Action 을 "서버에서 실행되는 함수" 정도로 외우는 것. 실제 구현은 client 측 stub + 서버 측 라우터 의 한 쌍이며, 이 분리가 보안 경계를 만든다.
읽는 순서
- 1이론
React 공식 RSC 문서와 본문의 "멘탈 모델" 절을 읽고 server 빌드 와 client 빌드 의 차이를 한 문단으로 적는다.
- 2구현
Forket 예제를 클론해서
<Page>가 server,<Comments>가 client 인 케이스를 만든다. 빌드 산출물에서 server 측<template type="forket/start/...">마커가 어떻게 들어가는지 직접 grep. - 3실무
Next.js 프로젝트에서 "server 컴포넌트로 두면 안 되는 것"(브라우저 API 의존, useState, event handler) 을 찾아 client boundary 위치를 재설계.
- 4설명
"
'use client'디렉티브가 빌드와 런타임에서 각각 무슨 일을 하나" 를 5분 발표. server 그래프 → boundary placeholder → client 측 hydration 의 3단계로 풀어낸다.
면접 연결 질문
[감점 답변] "RSC 는 새 SSR" / "성능이 좋다". [좋은 답변] SSR 은 HTML 만, RSC 는 HTML + 직렬화 트리 + client island bootstrap. RSC payload 는 client 상태를 유지한 채 server 컴포넌트만 부분 갱신할 수 있다는 점, server 컴포넌트는 client 번들에 포함되지 않는다는 점을 짚는다.
[감점 답변] "React 가 막아서". [좋은 답변] boundary 를 넘을 때 props 가 JSON 직렬화 되는데 함수는 직렬화 표현이 없다. 우회는 "use server" 액션을 정의해 함수 대신 Server Action ID 를 직렬화하는 것.
[좋은 답변] ① 빌드 단계에서 "이 파일은 server 인가 client 인가" 를 결정하는 그래프, ② 런타임에서 server 가 stream 으로 보낸 boundary 를 client 에서 hydration 하는 글루(<template> + bootstrap script).
자기 점검
"React 의 제약" 으로 답하는 것. 실제 원인은 boundary 를 넘는 props 가 JSON 으로 표현돼야 한다는 데 있다.
"서버에서만 실행되니까" 가 아니라, client 빌드에서 함수 본문이 RPC stub 으로 치환되기 때문이다.