서버 렌더(Server-Side Rendering, SSR)에서도 컴포넌트 함수는 실제로 호출되고, 그 안의 훅도 호출은 된다. 다만 서버에는 커밋(DOM 반영)이 없어서, 렌더 중 동기적으로 값을 계산하는 훅만 일하고 커밋 뒤에 도는 훅은 아무것도 하지 않는다. "이 훅은 클라이언트에서만 동작한다"는 말은 대개 "Effect 안의 일은 클라이언트에서만 일어난다"는 뜻이다.
| 훅 | 서버에서 |
|---|---|
useState(초기화 함수)·useReducer | 초기화 함수까지 실행해 초기 state를 만든다 |
useMemo·useCallback·useRef·useContext | 그대로 실행 |
useSyncExternalStore | subscribe는 부르지 않고 getServerSnapshot()의 값만 쓴다 |
useEffect·useLayoutEffect·useInsertionEffect | 아무것도 하지 않음(no-op) |
- React 19.2의 서버 렌더러 소스에서 Effect 계열 훅은 빈 함수이고,
useSyncExternalStore는getServerSnapshot()을 반환할 뿐이다(2026 기준). 경고 없이 조용히 무시된다 getServerSnapshot을 빠뜨리면 서버에서 에러가 나고, 가장 가까운 Suspense 경계부터 클라이언트 렌더로 넘어간다(CSR bailout). 서버에서 의미 있는 값이 없을 때 일부러 이렇게 클라이언트 전용으로 미루기도 한다(useSyncExternalStore)getServerSnapshot은 하이드레이션 때도 쓰인다. 서버와 클라이언트가 같은 값을 내야 하이드레이션 불일치가 나지 않는다
렌더 중 부수 효과는 서버에서 쌓인다
브라우저에서는 탭을 닫으면 끝나지만, 서버 프로세스는 수많은 요청을 오래 처리한다. 렌더 경로(초기화 함수, useMemo, 컴포넌트 본문)에서 생긴 부수 효과는 Effect처럼 건너뛰어지지 않으니 요청마다 실행되고 정리되지 않으면 누적된다. 렌더를 순수하게 두라는 React의 규칙과 렌더 순수성가 서버에서는 메모리 문제로 돌아온다.
TanStack Query의 useQuery가 좋은 예다. 서버에서도 useState 초기화 함수로 옵저버를 만들고, 그 과정에서 쿼리를 캐시에 등록한다. 데이터 요청(fetch)과 구독은 Effect·subscribe 쪽이라 서버에서 일어나지 않지만, 쿼리 등록과 GC 타이머 예약은 동기 코드라 서버에서도 일어난다.
useQuery → useState(() => new QueryObserver) → 캐시에 Query 등록 → scheduleGc()
gcTime이 유한하면 setTimeout(gcTime) 예약
→ 타이머가 Query를, Query가 그 요청의 QueryCache 전체를 붙잡는다TanStack Query v5 공식 SSR 가이드와 소스로 확인한 점(2026 기준, v5.104).
- 서버의
gcTime기본값은Infinity(브라우저는 5분)다. Infinity면 타이머를 아예 만들지 않아서, 요청이 끝나면 그 요청의 캐시가 통째로 가비지 컬렉션(Garbage Collection) 대상이 된다 - 훅이나
defaultOptions에서 유한한gcTime을 직접 주면 서버에서도 그 시간만큼 타이머가 살아 있다. 24시간처럼 길게 주면 요청마다 만든 캐시가 그만큼 메모리에 남고, 타이머 하나가 같은 캐시의 다른 쿼리까지 함께 붙잡는다. 가이드는 이 경우 "캐시를 직접 일찍 비울 책임이 있다"고 한다(예:dehydrate뒤queryClient.clear()) - 서버에서만
gcTime: 0으로 분기하는 방법도 쓰이지만, 가이드는0이 하이드레이션 에러를 낼 수 있으니 피하라고 한다. 서버에서는 유한값으로 덮어쓰지 않는 쪽(기본값 Infinity 유지)이 먼저다 - 반대로
QueryClient를 모듈 최상위에서 하나만 만들면 메모리 문제가 아니라 모든 사용자가 같은 캐시를 공유해 데이터가 다른 사용자에게 섞이는 문제가 된다. 요청마다(컴포넌트 안 state로) 만든다 - SSR에서는 클라이언트가 받자마자 다시 요청하지 않도록
staleTime을 0보다 크게 두라고 권한다(staleTime과 gcTime)
서버 메모리가 계단처럼 오르기만 하면 힙 스냅샷에서 무엇이 붙잡고 있는지(retainer)를 따라가 GC 루트(활성 타이머, 전역 변수, 모듈 스코프 캐시)를 찾는다(크롬 개발자 도구 팁).
출처: React - useEffect: Caveats · React - useSyncExternalStore: Adding support for server rendering · TanStack Query - Server Rendering & Hydration