Overview
이전 글에서 평점 데이터 분리 문제를 해결했지만, 또 다른 문제가 남아있었다. 바로 새로고침 시 발생하는 UI Flickering이었다. 사용자가 F5를 누르면 스켈레톤 UI가 잠깐 보이다가 실제 평점으로 바뀌는 현상이 발생했다. 이 문제를 React Query의 SSR Hydration으로 어떻게 해결했는지를 기록한다.
문제 상황: Flickering이 발생하는 이유
React Query의 메모리 캐시 특성
React Query는 기본적으로 브라우저 메모리에만 캐시를 저장한다. 이는 다음과 같은 특성을 가진다:
- 페이지 새로고침(F5) 시 모든 메모리 상태가 초기화됨
- 브라우저 탭을 닫고 다시 열면 캐시가 사라짐
- JavaScript 실행 전까지는 캐시 데이터에 접근할 수 없음
이는 React Query가 브라우저 환경에서 동작하는 클라이언트 전용 라이브러리이기 때문이다. 서버에 캐시를 저장하거나 공유하지 않으며, 기본적으로 페이지 이동이나 새로고침 시 캐시는 초기화된다.
새로고침 시 발생하는 문제
🔄 Before: SSR Hydration 없을 때
이 과정에서 사용자는 스켈레톤 UI → 실제 평점으로 바뀌는 UI Flickering을 경험하게 된다.
해결책: SSR Hydration
SSR Hydration은 이 문제를 서버와 클라이언트 상태를 동기화함으로써 해결한다:
⚡ After: SSR Hydration 적용 후
구현 과정
1. 서버에서 데이터 미리 가져오기
// 평점 데이터 프리패칭
await Promise.allSettled(
lectures.map(lecture =>
queryClient.prefetchQuery({
queryKey: ['lecture-rating', lecture.id, universityName],
queryFn: () => getLectureRating(lecture.id, universityName)
})
)
);
const dehydratedState = dehydrate(queryClient);2. 클라이언트로 캐시 상태 전달
<HydrationBoundary state={dehydratedState}>
<LectureList lectures={lectures} />
</HydrationBoundary>작동 원리
Hydration 과정
- 서버 렌더링: QueryClient가 서버에서 생성되고 API 호출로 데이터를 미리 가져옴
- 상태 직렬화:
dehydrate()로 캐시 상태를 JSON으로 직렬화하여 HTML과 함께 전송 - 클라이언트 Hydration: 브라우저가 HTML을 즉시 렌더링하고, JavaScript 로드 후 캐시 상태를 복원
- 동기화 완료: React Query 훅들이 서버 캐시를 즉시 사용하여 Flickering 방지
또한 이 과정에서 실제 프로덕션 환경에서는 몇 가지 중요한 고려사항이 있다.
참고: React Query는 hydration 후에도 background refetch를 시도할 수 있으므로, 초기 Hydration 상태를 유지하려면
staleTime을 적절히 설정하여 background refetch를 지연시켜야 한다.
보안 고려사항
서버에서 클라이언트로 데이터를 직렬화하여 전송하는 과정에서 보안 이슈가 발생할 수 있다.
데이터 직렬화 보안
dehydrate() 과정에서 서버 데이터가 클라이언트로 직렬화되는데, 이때 민감한 정보가 포함되지 않도록 주의해야 한다.
만약에 민감할 수 있는 토큰의 데이터가 들어있다면?
// 안전하지 않은 예시
const dehydratedState = dehydrate(queryClient); // 모든 캐시 데이터 포함
// 안전한 예시
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// 민감한 데이터는 제외
return !query.queryKey.some(k => typeof k === 'string' && k.includes('user-token'));
}
});에러 핸들링
서버에서 데이터 prefetch가 실패해도 클라이언트 렌더링이 차단되지 않도록 Promise.allSettled를 사용했다.
// 일부 데이터 로딩 실패 시에도 페이지는 정상 렌더링
await Promise.allSettled(
lectures.map(lecture =>
queryClient.prefetchQuery({
queryKey: ['lecture-rating', lecture.id, universityName],
queryFn: () => getLectureRating(lecture.id, universityName)
})
)
);결과
사용자 경험 개선
| 구분 | Before (Hydration 없음) | After (Hydration 적용) |
|---|---|---|
| 초기 렌더링 | 스켈레톤 UI → 실제 데이터 | 즉시 완전한 데이터 표시 |
| 렌더링 속도 | ~200ms (체감 지연) | 체감상 즉시 |
| 사용자 경험 | Flickering 발생 | 깜빡임 없는 즉시 로딩 |
| 데이터 신뢰성 | 클라이언트 요청 의존 | 서버 검증 포함 |
마무리
React Query의 SSR Hydration은 서버와 클라이언트 간의 상태 동기화를 자동으로 처리해주어, 복잡한 상태 관리 없이도 매끄러운 UX를 구현할 수 있게 해준다.
작은 UI Flickering 현상이었지만, 이를 해결하는 과정에서 React Query의 SSR Hydration에 대해 깊이 이해할 수 있었다. 무엇보다 사용자가 새로고침을 해도 완전히 매끄러운 경험을 제공할 수 있게 되어 큰 만족감을 느꼈다.