React Server Components와 싸우다 만든 나만의 상태 관리 라이브러리
(React Server Components, 상태 관리 라이브러리, React 상태 관리)
# 서론 – “서버와 클라이언트 사이의 데이터 공유, 왜 이렇게 어려울까?”
React Server Components(RSC)를 도입한 뒤, 복잡한 페이지를 만들면서 가장 크게 부딪힌 문제는 서버와 클라이언트 간에 상태를 어떻게 공유하느냐였습니다.
RSC는 서버에서 렌더링된 UI를 그대로 클라이언트에 전달해 성능을 높여 주지만, 그 과정에서 기존에 사용하던 Redux·Recoil·Zustand 같은 클라이언트‑전용 상태 관리 도구는 제 역할을 못하게 됩니다.
저는 6개월 동안 여러 프로젝트를 진행하면서 “서버‑사이드에서 만든 데이터를 클라이언트에서도 즉시 활용하고 싶다”는 요구에 부딪혔고, 결국 직접 상태 관리 라이브러리를 만들기로 결심했습니다. 이번 포스트에서는 그 과정을 초보자도 이해하기 쉽도록 단계별로 정리합니다.
# 1️⃣ React Server Components의 기본 동작 원리
서버에서 컴포넌트 실행
export default async function Component() { … }형태로 선언하면 서버에서만 실행됩니다.
클라이언트에 스트리밍 전송
- 서버가 만든 HTML/JSX가 스트리밍 방식으로 클라이언트에 전달됩니다.
클라이언트는 “읽기 전용” UI만 받음
- 클라이언트는 해당 컴포넌트를 다시 렌더링하거나 상태를 직접 바꿀 수 없습니다.
👉 핵심 포인트: RSC는 데이터를 가져오는 역할은 하지만, 데이터를 수정하거나 유지하는 역할은 클라이언트가 담당한다는 점입니다.
# 2️⃣ 기존 상태 관리 도구가 RSC와 맞지 않았던 이유
| 도구 | 서버 사이드 지원 | 클라이언트와의 동기화 방식 | 문제점 |
|------|----------------|--------------------------|--------|
| Redux | 제한적 (middleware 필요) | 전역 스토어를 직접 공유 | 서버에서 만든 상태를 클라이언트에 자동 전파 불가 |
| Recoil | 클라이언트 전용 | Atom/Selector 기반 | RSC와 결합 시 서버에서 만든 atom을 초기화할 방법이 없음 |
| Zustand | 경량 | Hook 기반 | 서버 렌더링 시 상태 초기값을 전달하는 메커니즘 부재 |
결과적으로 서버에서 만든 데이터를 클라이언트가 그대로 사용할 방법이 없었고, 매번 API 호출을 중복해서 수행하거나, 복잡한 프록시 로직을 구현해야 했습니다.
# 3️⃣ 내가 만든 상태 관리 라이브러리의 설계 원칙
# 3.1 서버‑클라이언트 “동기화 파이프라인” 구축
- 서버에서
serializeState함수 호출
ts
// server/state.ts
export function serializeState(data) {
return JSON.stringify(data);
}
- 클라이언트에서
hydrateState로 복구
ts
// client/state.ts
export function hydrateState(serialized) {
return JSON.parse(serialized);
}
이 파이프라인을 통해 서버가 만든 JSON 형태의 상태를 클라이언트가 최초 렌더링 시 그대로 복원합니다.
# 3.2 React Context와 Hook을 결합
tsx
const StateContext = React.createContext<any>(null);
export const StateProvider = ({initialState, children}) => {
const [state, setState] = React.useState(initialState);
return (
<StateContext.Provider value={{state, setState}}>
{children}
</StateContext.Provider>
);
};
export const useAppState = () => React.useContext(StateContext);
Context는 전역 접근을 보장하고,
Hook은 사용 편의성을 제공해 초보자도
const {state, setState} = useAppState();한 줄로 사용할 수 있습니다.
# 3.3 자동 “서버‑클라이언트 재동기화”
서버에서 상태가 변하면 serializeState를 다시 호출하고, 클라이언트는 EventSource(SSE) 혹은 WebSocket을 통해 변화를 감지해 hydrateState를 실행합니다. 이렇게 하면 실시간으로 상태가 동기화됩니다.
# 4️⃣ 실제 적용 예시 – 간단한 Todo 앱
tsx
// server/todo.ts (RSC)
export async function TodoList() {
const todos = await db.todo.findMany();
const serialized = serializeState(todos);
return (
<>
<script dangerouslySetInnerHTML={{
__html: `window.__INITIAL_TODOS__ = ${serialized};`
}}/>
<TodoListClient />
</>
);
}
tsx
// client/TodoListClient.tsx
import {useEffect} from 'react';
import {StateProvider, useAppState} from '../state';
function TodoListClient() {
const {state: todos, setState: setTodos} = useAppState();
useEffect(() => {
// 최초 로드 시 서버에서 전달된 상태 복원
if (window.__INITIAL_TODOS__) {
setTodos(hydrateState(window.__INITIAL_TODOS__));
}
}, []);
// UI 렌더링 …
}
서버는 데이터를 가져와 직렬화하고,
클라이언트는 페이지 로드 시
hydrateState로 복구해 즉시 UI에 반영합니다.
이 구조 덕분에 중복 API 호출 없이 서버와 클라이언트가 동일한 데이터를 공유하게 됩니다.
# 5️⃣ 배포와 성능 최적화 팁
JSON 압축 –
gzip혹은brotli로 직렬화된 상태를 압축하면 네트워크 비용이 크게 감소합니다.부분 상태 전송 – 전체 상태가 아닌 변경된 부분만 전송하도록 diff 알고리즘을 적용하면 실시간 동기화가 더욱 가볍게 동작합니다.
SSR 캐시 활용 – 동일한 요청에 대해
serializeState결과를 캐시하면 서버 부하를 크게 낮출 수 있습니다.
# 결론 – 직접 만든 상태 관리 라이브러리로 RSC를 자유롭게!
React Server Components와 기존 상태 관리 도구 사이의 격차는 서버‑클라이언트 상태 동기화 파이프라인을 직접 설계함으로써 해소할 수 있습니다.
서버에서는 데이터를 직렬화해 전달하고,
클라이언트에서는
hydrateState로 복원해 전역 Context에 저장한다.
이 방식을 적용하면 중복 API 호출을 없애고, 초기 로딩 속도를 크게 개선할 수 있습니다.
지금 바로 여러분의 프로젝트에 위와 같은 간단한 상태 관리 구조를 도입해 보세요. 직접 구현해 보면 React Server Components와의 궁합이 얼마나 좋아지는지 체감하실 수 있을 겁니다.
궁금한 점이나 구현 중 마주친 문제는 댓글로 남겨 주세요! 여러분의 피드백이 다음 개선에 큰 도움이 됩니다. 🚀
Comments
Join the discussionNo comments yet. Be the first to comment.