React → Next.js(App Router) 마이그레이션 회고: ‘클라이언트 사고’에서 벗어나기까지
- 🚀 프로젝트 배경 & 마이그레이션 결심 이유
왜 React → Next.js App Router로 옮겼는가?
React로 만든 기존 프로젝트는 순수 SPA였기 때문에 TMDB나 OpenAI API를 호출할 때 API Key가 클라이언트 네트워크 탭에 그대로 노출되는 문제가 있었습니다. 배포를 하려고 하니 민감 정보때문에 서버가 필요했고, 단순히 별도 프록시 서버를 구축하는 대신, Next.js를 도입하여 하나의 프로젝트 내에서 통합 관리하는 것이 장기적인 배포와 유지보수 측면에서 가장 효율적이라고 판단하여 마이그레이션을 결정했습니다.
초기에는 기존 코드 수정을 최소화하기 위해 클라이언트 중심의 Hydration 작업 위주로 진행했습니다. 이 과정에서 겪었던 큰 시행착오 중 하나는 "클라이언트 방식의 사고"를 서버 환경에 그대로 적용하려 했던 점이였습니다.
- 🤯 “클라이언트 방식의 사고” 그대로 시작한 첫 번째 실패
가장 큰 오해는 'use client'를 사용하면 해당 코드가 "클라이언트에서만 실행될 것"이라고 생각했던 것입니다.
최초 로딩 시에는 Server Component와 Client Component 모두 서버(Node.js 환경)에서 한 번 실행되어 HTML을 만든다는 사실을 뒤늦게 알게 되었습니다.
이 오해로 인해 환경 변수/URL 문제가 서버 환경에서 발생했습니다. 이는 초기 렌더링과 이후 페이지 이동의 렌더링 과정이 다르다는 중요한 인지를 얻는 계기가 되었습니다.
- Hydration 지옥: GPT API의 동적 응답 문제와 한계
특히 GPT API의 영화 리뷰에 대한 응답이 요청마다 달라지기 때문에, 서버 렌더링 결과와 클라이언트 렌더링 결과가 달라져 Hydration Mismatch 에러가 빈번하게 발생했습니다.
이를 해결하기 위해, 서버에서 prefetchQuery로 데이터를 미리 가져오고, dehydrate() 후, HydrationBoundary를 통해 클라이언트로 전달하는 방식으로 서버와 클라이언트가 동일한 데이터를 사용하도록 했습니다.
이 경험을 통해 Hydration의 원리를 정확하게 짚고 넘어갈 수 있었습니다.
하지만 이러한 React Query SSR 기반 Hydration 방식은 전체 데이터 프리패치가 완료되어야만 HTML을 생성하여 전송하기 때문에 TTFB가 크게 증가했고 , Next.js의 핵심 기능인 Suspense, 스트리밍, 서버 캐싱 등을 제대로 활용할 수 없었습니다.
- 💥 패러다임 전환: RSC 중심 구조로 재설계
Hydration 기반 설계의 한계를 명확히 인지하고, 결국 구조 자체를 RSC중심으로 다시 마이그레이션하였습니다.
RSC의 본질 이해: JSON Payload 기반 Partial Rendering
이 과정에서 Server Component가 전통적인 SSR과 근본적으로 다르다는 것을 깨달았습니다.
Server Component는 완전한 HTML을 생성하는 것이 아니라, 컴포넌트 트리를 JSON 형태(RSC Payload)로 직렬화하여 전달합니다. 이러한 구조 덕분에 Streaming, TTFB 개선, Suspense를 통한 부분 로딩 관리가 가능하다는 핵심 원리를 이해할 수 있었습니다.
실제로 레이아웃은 즉시 보여주고 영화 데이터는 로딩 표시 후 스트리밍으로 채워지는 방식으로, 이전 React Query SSR 대비 훨씬 나은 사용자 경험을 제공했습니다.
아키텍처 로직 분리 및 최적화
RSC 중심으로 전환하며 다음의 핵심 아키텍처 변경을 단행했습니다.
상태 관리 전환: 전역 상태로 관리하던 언어 설정 (
useLanguageStore등 클라이언트 상태) → URL Path Parameter로 변경하여 서버/클라이언트 간 상태 공유 및 SEO를 최적화했습니다.데이터 페칭 최적화:
axios→ 네이티브fetch로 전환하여 Next.js의 캐싱 메커니즘과 통합했습니다.캐싱 전략 도입: Next.js 캐싱 옵션인
revalidate및force-cache를 활용하여 API 호출 최적화와 서버 자원 사용량 절감을 동시에 달성했습니다.
무한 스크롤 API 설계 결정 (성능/비용 최적화)
무한 스크롤 API 호출 시, 민감 정보(API Key)를 클라이언트에 노출하지 않으면서도 효율적으로 데이터를 가져오기 위해 Server Actions와 API Routes 중 어떤 방식을 사용할지 고민했습니다.
Server Actions는 주로 데이터 변경(Mutation)과 자동 UI 업데이트에 초점이 맞춰져 있으며, 잦은 조회(Read)에 사용될 경우 POST 요청의 캐싱 비효율성 및 RSC 렌더링 파이프라인 오버헤드로 인해 비용과 성능 면에서 불리함을 파악했습니다.
따라서 최종적으로 API Routes (GET 요청)를 사용하여 API Key 보안을 유지하면서도, HTTP GET의 캐싱 장점을 극대화하여 무한 스크롤 기능을 효율적으로 구현했습니다.
- 느낀점
이번 마이그레이션은 단순히 React에서 Next.js로 코드를 옮기는 작업 이상의 의미가 있었습니다. 처음에는 배포를 위해 "API Key를 숨기려면 서버가 필요하다"는 단순한 동기로 시작했지만,
문서만 읽었을때와 실제로 부딪혀보면서 적용하니 체감이 달랐습니다.
Hydration Mismatch를 겪으며, 서버/클라이언트 렌더링 경계를 명확히 해야 하는 이유를 체감했습니다. 이는 단순히 에러를 피하는 것이 아니라, 어떤 로직을 서버에 두고, 어떤 로직을 클라이언트로 보내야 하는가"에 대한 명확한 설계 기준을 확립하는 계기가 되었습니다. 또한 React SPA → Pages Router → App Router로 이어지는 프론트엔드 렌더링 패러다임의 진화를 압축적으로 경험할 수 있었고, 각 기술이 등장한 배경과 해결하려는 문제가 무엇인지, 그리고 프론트엔드 생태계가 "서버와 클라이언트의 장점을 각각 활용"하는 방향으로 진화하고 있다는 것을 이해하게 되었습니다.
이번 마이그레이션을 통해 얻은 가장 큰 자산은 "왜 이렇게 동작하는가?"를 스스로 생각해보고. 앞으로 새로운 문제에 직면했을 때,
근본 원인을 프레임워크 동작 원리 관점에서 진단
여러 해결책의 트레이드오프 분석
프로젝트 요구사항에 최적화된 솔루션 도출
이러한 엔지니어링 사고 프로세스를 확립할 수 있었습니다.