Next.js에서 Vite로 마이그레이션, 빌드 10분에서 2분 미만으로 단축
Railway가 프로덕션 프론트엔드를 Next.js에서 Vite + TanStack Router로 성공적으로 마이그레이션한 후기를 공유했습니다. 기존 Next.js의 서버 중심 패턴은 클라이언트 중심 앱에 맞지 않았고, 빌드 시간이 10분을 넘어서는 등 병목이 되었습니다. 두 번의 PR과 무중단 배포를 통해 200개 이상의 라우트 마이그레이션을 완료하며, 클라이언트 중심 개발에 적합한 스택의 중요성을 보여줍니다.
Victor Ramirez 2026년 4월 3일
Railway의 전체 프로덕션 프론트엔드가 더 이상 Next.js에서 실행되지 않습니다. 대시보드, 캔버스, railway.com 등 모든 것이 이제 Vite + TanStack Router에서 실행되며, 우리는 이 마이그레이션을 두 개의 PR(Pull Request)로 무중단 배포했습니다.
Next.js는 우리에게 잘 맞았습니다. 그러다 더 이상 그렇지 않게 되었습니다. Next.js는 railway.com을 월간 수백만 명의 사용자에게 서비스하는 프로덕션 앱으로 성장시키는 데 도움을 주었습니다. 훌륭한 프레임워크지만, 우리 제품에는 더 이상 맞는 선택이 아니었습니다.
프론트엔드 빌드가 10분을 넘어서기 시작했습니다. 그중 6분은 Next.js 단독으로 소요되었고, 절반은 '페이지 최적화 마무리(finalizing page optimization)' 단계에서 멈춰 있었습니다. 하루에도 여러 번 배포하는 팀에게 이런 빌드 시간은 단순한 성가신 문제가 아닙니다. 이는 모든 개발 주기에 부과되는 매우 비싼 세금과 같습니다.
Railway의 앱은 압도적으로 클라이언트 측(client-side)입니다. 대시보드는 상태를 많이 관리하는 풍부한 인터페이스입니다. 캔버스는 실시간입니다. 웹소켓(WebSockets)이 곳곳에 사용됩니다. Next.js의 서버 우선 기본 요소(server-first primitives)는 우리가 사용하지 않는 것이었고, 결국 레이아웃과 라우팅 문제를 지원하기 위해 Pages Router 위에 우리 자신의 추상화를 구축해야 했습니다. 왜냐하면 프레임워크가 우리가 필요로 하는 방식으로 이를 처리하지 않았기 때문입니다.
우리는 여전히 Pages Router를 사용하고 있었고, 이는 공유 레이아웃을 엉성하게 만들었습니다. 모든 레이아웃 패턴이 일급 프레임워크 기본 요소가 아니라 억지로 끼워 넣은 우회책이었습니다. App Router가 이러한 문제 중 일부를 해결했겠지만, 이는 서버 우선 패턴을 크게 강조하며, 우리의 제품은 의도적으로 클라이언트 주도형(client-driven)입니다. 이를 채택하는 것은 우리에게 필요하지 않은 패러다임을 중심으로 재구축하는 것을 의미했을 것입니다.
왜 TanStack Start + Vite인가 우리는 우리가 실제로 구축하는 방식, 즉 명시적이고(explicit), 클라이언트 우선이며(client-first), 빠르게 반복할 수 있는 스택을 원했습니다. 또한 우리가 이를 진심으로 즐겁게 사용할 수 있다는 것도 중요했습니다. 제품 팀에게 있어 프론트엔드 구현 방법에 대해 고민하지 않아도 되는 몇 가지 편의 기능을 원했고, 다음 사항들이 우리를 확신시켰습니다.
- 기본 제공되는 타입 안전 라우팅(Type-safe routing). 라우트 매개변수와 검색 매개변수가 추론되고, 자동 완성이 전체 라우트 트리에서 작동하며, 라우트 자체는 파일 시스템에서 생성됩니다.
- 일급 레이아웃(First-class layouts). 경로가 없는 레이아웃 라우트(Pathless layout routes)가 이전의 모든 편법을 조합 가능하고 예측 가능한 것으로 대체했습니다.
- 생각할 필요가 없을 정도로 빠른 개발 루프. 즉각적인 HMR(Hot Module Replacement), 0에 가까운 시작 시간. 코드를 변경하고 결과를 확인하는 사이의 피드백 주기가 사실상 사라집니다.
- 실제로 필요한 곳에서만 사용하는 SSR(Server-Side Rendering). 마케팅 페이지, 변경 로그, 채용 페이지 등에만 사용합니다. 그 외의 모든 곳에서는 순수 클라이언트 측 렌더링을 사용합니다. 이점이 없는 화면에 서버 렌더링을 강요하지 않기 때문입니다. (편집자 주: 아이러니하게도 이 블로그는 아직 TanStack 기반이 아닙니다.)
- 명시적인 모델(Explicit model). 즉, TanStack은 프레임워크의 '마법'에 의존하지 않고 내부 작동 방식에 대한 제어를 더 많이 제공한다는 의미입니다.
우리 중 몇 명이 연말 연시에 TanStack Start를 사용해 보았고 반응은 만장일치였습니다. 우리는 그것으로 구축하는 것을 좋아하며, Railway의 대시보드와 같은 제품에서는 그것이 벤치마크만큼이나 중요합니다.
두 번의 PR, 무중단 배포 선택을 한 번, 나는 작업을 시작했습니다. 병합 전 스쿼시(squash) 이전에, 아마 수백 번의 커밋을 했을 것입니다. 월간 수백만 명의 사용자에게 서비스하는 200개 이상의 라우트를 가진 프로덕션 프론트엔드를 마이그레이션하는 것은 보통 병렬 실행과 점진적 전환이 필요해 몇 달이 걸리는 작업입니다. 우리는 마감일이 있었기 때문에 두 개의 풀 리퀘스트로 완료했습니다.
PR 1은 Next.js에만 해당하는 모든 것을 교체했습니다: next/image, next/head, next/router. 각각은 네이티브 브라우저 API나 프레임워크에 구애받지 않는 대안으로 교체되었습니다. 이 PR은 프레임워크 자체를 전혀 변경하지 않았습니다. 단지 프레임워크에 대한 모든 의존성을 제거했을 뿐이며, 그 덕분에 PR 2는 깔끔한 교체가 될 수 있었습니다.
PR 2는 프레임워크를 교체했습니다. 200개 이상의 라우트가 마이그레이션 되었습니다. 우리는 체계적으로 페이지 파일에서 라우팅과 관련 없는 모든 것을 개별 React 컴포넌트로 먼저 추출한 다음, 원래 페이지 트리에서 모든 라우트를 생성했습니다. 그런 다음 Nitro를 서버 계층으로 추가하고 next.config.js를 Nitro 설정으로 대체하여 리디렉션(500개 이상), 보안 헤더 및 캐싱 규칙을 한 곳으로 통합했습니다. 또한... (원문 누락)