Skip to main content

Command Palette

Search for a command to run...

React → Next.js(App Router) 마이그레이션 회고: ‘클라이언트 사고’에서 벗어나기까지

Updated
3 min readView as Markdown
D

프론트엔드 개발자 doit입니다.

  1. 🚀 프로젝트 배경 & 마이그레이션 결심 이유

왜 React → Next.js App Router로 옮겼는가?

React로 만든 기존 프로젝트는 순수 SPA였기 때문에 TMDB나 OpenAI API를 호출할 때 API Key가 클라이언트 네트워크 탭에 그대로 노출되는 문제가 있었습니다. 배포를 하려고 하니 민감 정보때문에 서버가 필요했고, 단순히 별도 프록시 서버를 구축하는 대신, Next.js를 도입하여 하나의 프로젝트 내에서 통합 관리하는 것이 장기적인 배포와 유지보수 측면에서 가장 효율적이라고 판단하여 마이그레이션을 결정했습니다.

초기에는 기존 코드 수정을 최소화하기 위해 클라이언트 중심의 Hydration 작업 위주로 진행했습니다. 이 과정에서 겪었던 큰 시행착오 중 하나는 "클라이언트 방식의 사고"를 서버 환경에 그대로 적용하려 했던 점이였습니다.


  1. 🤯 “클라이언트 방식의 사고” 그대로 시작한 첫 번째 실패

가장 큰 오해는 'use client'를 사용하면 해당 코드가 "클라이언트에서만 실행될 것"이라고 생각했던 것입니다.

최초 로딩 시에는 Server Component와 Client Component 모두 서버(Node.js 환경)에서 한 번 실행되어 HTML을 만든다는 사실을 뒤늦게 알게 되었습니다.

이 오해로 인해 환경 변수/URL 문제가 서버 환경에서 발생했습니다. 이는 초기 렌더링과 이후 페이지 이동의 렌더링 과정이 다르다는 중요한 인지를 얻는 계기가 되었습니다.

  1. Hydration 지옥: GPT API의 동적 응답 문제와 한계

특히 GPT API의 영화 리뷰에 대한 응답이 요청마다 달라지기 때문에, 서버 렌더링 결과와 클라이언트 렌더링 결과가 달라져 Hydration Mismatch 에러가 빈번하게 발생했습니다.

이를 해결하기 위해, 서버에서 prefetchQuery로 데이터를 미리 가져오고, dehydrate() 후, HydrationBoundary를 통해 클라이언트로 전달하는 방식으로 서버와 클라이언트가 동일한 데이터를 사용하도록 했습니다.
이 경험을 통해 Hydration의 원리를 정확하게 짚고 넘어갈 수 있었습니다.

하지만 이러한 React Query SSR 기반 Hydration 방식은 전체 데이터 프리패치가 완료되어야만 HTML을 생성하여 전송하기 때문에 TTFB가 크게 증가했고 , Next.js의 핵심 기능인 Suspense, 스트리밍, 서버 캐싱 등을 제대로 활용할 수 없었습니다.


  1. 💥 패러다임 전환: RSC 중심 구조로 재설계

Hydration 기반 설계의 한계를 명확히 인지하고, 결국 구조 자체를 RSC중심으로 다시 마이그레이션하였습니다.

RSC의 본질 이해: JSON Payload 기반 Partial Rendering

이 과정에서 Server Component가 전통적인 SSR과 근본적으로 다르다는 것을 깨달았습니다.

Server Component는 완전한 HTML을 생성하는 것이 아니라, 컴포넌트 트리를 JSON 형태(RSC Payload)로 직렬화하여 전달합니다. 이러한 구조 덕분에 Streaming, TTFB 개선, Suspense를 통한 부분 로딩 관리가 가능하다는 핵심 원리를 이해할 수 있었습니다.

실제로 레이아웃은 즉시 보여주고 영화 데이터는 로딩 표시 후 스트리밍으로 채워지는 방식으로, 이전 React Query SSR 대비 훨씬 나은 사용자 경험을 제공했습니다.

아키텍처 로직 분리 및 최적화

RSC 중심으로 전환하며 다음의 핵심 아키텍처 변경을 단행했습니다.

  1. 상태 관리 전환: 전역 상태로 관리하던 언어 설정 (useLanguageStore 등 클라이언트 상태) → URL Path Parameter로 변경하여 서버/클라이언트 간 상태 공유 및 SEO를 최적화했습니다.

  2. 데이터 페칭 최적화: axios → 네이티브 fetch로 전환하여 Next.js의 캐싱 메커니즘과 통합했습니다.

  3. 캐싱 전략 도입: Next.js 캐싱 옵션인 revalidateforce-cache를 활용하여 API 호출 최적화와 서버 자원 사용량 절감을 동시에 달성했습니다.

무한 스크롤 API 설계 결정 (성능/비용 최적화)

무한 스크롤 API 호출 시, 민감 정보(API Key)를 클라이언트에 노출하지 않으면서도 효율적으로 데이터를 가져오기 위해 Server Actions와 API Routes 중 어떤 방식을 사용할지 고민했습니다.

Server Actions는 주로 데이터 변경(Mutation)과 자동 UI 업데이트에 초점이 맞춰져 있으며, 잦은 조회(Read)에 사용될 경우 POST 요청의 캐싱 비효율성 및 RSC 렌더링 파이프라인 오버헤드로 인해 비용과 성능 면에서 불리함을 파악했습니다.

따라서 최종적으로 API Routes (GET 요청)를 사용하여 API Key 보안을 유지하면서도, HTTP GET의 캐싱 장점을 극대화하여 무한 스크롤 기능을 효율적으로 구현했습니다.

  1. 느낀점

이번 마이그레이션은 단순히 React에서 Next.js로 코드를 옮기는 작업 이상의 의미가 있었습니다. 처음에는 배포를 위해 "API Key를 숨기려면 서버가 필요하다"는 단순한 동기로 시작했지만,

문서만 읽었을때와 실제로 부딪혀보면서 적용하니 체감이 달랐습니다.

Hydration Mismatch를 겪으며, 서버/클라이언트 렌더링 경계를 명확히 해야 하는 이유를 체감했습니다. 이는 단순히 에러를 피하는 것이 아니라, 어떤 로직을 서버에 두고, 어떤 로직을 클라이언트로 보내야 하는가"에 대한 명확한 설계 기준을 확립하는 계기가 되었습니다. 또한 React SPA → Pages Router → App Router로 이어지는 프론트엔드 렌더링 패러다임의 진화를 압축적으로 경험할 수 있었고, 각 기술이 등장한 배경과 해결하려는 문제가 무엇인지, 그리고 프론트엔드 생태계가 "서버와 클라이언트의 장점을 각각 활용"하는 방향으로 진화하고 있다는 것을 이해하게 되었습니다.

이번 마이그레이션을 통해 얻은 가장 큰 자산은 "왜 이렇게 동작하는가?"를 스스로 생각해보고. 앞으로 새로운 문제에 직면했을 때,

  1. 근본 원인을 프레임워크 동작 원리 관점에서 진단

  2. 여러 해결책의 트레이드오프 분석

  3. 프로젝트 요구사항에 최적화된 솔루션 도출

이러한 엔지니어링 사고 프로세스를 확립할 수 있었습니다.

24 views

More from this blog

Agent Skill로 README 자동화하기

저는 요즘 업무에서 AI 에이전트를 적극적으로 활용하여 반복적인 업무를 최대한 AI로 자동화하기 위해 공부 중인데요. 이번에 했던 작업 중 하나가 README 파일 생성 자동화입니다. AI에게 README를 써달라고 하면 꽤 그럴싸한 결과물이 나옵니다. 하지만 여러 프로젝트에 반복하다 보면 프롬프트가 모호할 때마다 매번 다른 방식으로 문제를 풀기 때문에 들

Mar 31, 20268 min read8

배열을 처리하는 세 가지 방법 (chaining, reduce, for loop)

개발을 하다 보면 배열 데이터를 가공할 일이 정말 많은데요.저는 평소에는 filter, map 같은 메서드 체이닝 방식을 주로 사용합니다. 읽기 쉽고, 데이터 흐름이 단계별로 명확하게 보이기 때문이죠. 하지만 어떠한 계기로(?) 인해 이런 의문을 가지게 되었습니다.. "만약 데이터가 수천만 건으로 늘어난다면? 배열을 여러 번 순회하는 체이닝 방식이 성능 병목을 일으키지는 않을까?" 혹은 "reduce 하나로 합치는 게 베스트일까?" 이 질문을 ...

Feb 4, 20263 min read19

[React] 좋은 추상화란 무엇인가(2) 잘못된 추상화는 중복보다 비싸다

지난 글을 보면 성급했던 재사용 컴포넌트 경험으로 고생을 한 적이 있었는데요.(아직 안 보셨다면? [1편: 좋은 추상화란 무엇인가(1)?] ) 애초에 왜 이런 고생을 했을까를 생각해 보면이유는 단순했습니다. 중복 코드가 거슬리니 바로 추상화를 해버린 것이었죠..똑같은 실수를 반복하지 않기 위해 저만의 기준이 필요하다고 느꼈습니다. 이에 대해 이미 깊이 고민하셨던 Cher Scarlett님과 Kent C. Dodds님의 글을 보면서, 제가 취해야...

Dec 16, 20253 min read25

[React] 좋은 추상화란 무엇인가(1) ? 유연한 컴포넌트 설계를 위한 제어의 역전

재사용을 위해 만든 컴포넌트가, 시간이 지날수록 오히려 재사용하기 어려워진 경험이 있으신가요? 저는 최근 프로젝트에서 SearchFilter 컴포넌트가 그러한데요 프로젝트에는 여러 목록 페이지가 존재했고, 페이지마다 검색 조건이 제각각이었습니다. 그래서 처음에는 Configuration 배열만 넘기면 알아서 그려주는 컴포넌트를 만들었습니다. // ❌ 초기의 설정 기반 접근 (Configuration) const filterFields = [ ...

Dec 9, 202510 min read36

React 시대의 함수형 프로그래밍

바닐라 JS처럼 개발자가 DOM과 상태 변경을 직접 제어하던 시기에는 불변성·순수 함수 같은 함수형 프로그래밍 철학이 지금처럼 중요하지는 않았습니다. 하지만 React(Vue/Redux/SWR/Recoil 등)처럼 선언적·상태 기반 UI가 등장하면서 상태를 “데이터 흐름의 스냅샷”으로 다뤄야 했고, 그때부터 함수형 프로그래밍이 중요해지기 시작했습니다. 🔥 바닐라 JS에서는 왜 FP가 별로 중요하지 않았을까? 바닐라 JS 시대의 UI 개발은...

Dec 8, 20255 min read35
D

Dlog!

39 posts

배우고, 나누며, 함께 성장하는 개발자 🚀