메뉴
HN
Hacker News 53일 전

Next.js에서 Vite로 마이그레이션, 빌드 10분에서 2분 미만으로 단축

IMP
8/10
핵심 요약

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개 이상), 보안 헤더 및 캐싱 규칙을 한 곳으로 통합했습니다. 또한... (원문 누락)

원문 보기
원문 보기 (영어)
Victor Ramirez Apr 3, 2026 Railway's entire production frontend no longer runs on Next.js. The dashboard, the canvas, railway.com , all of it now runs on Vite + TanStack Router, and we shipped the migration in two PRs with zero downtime. Next.js served us well. Then it didn't. Next.js got railway.com from zero to a production app serving millions of users monthly. It's an excellent framework, but it stopped being the right one for our product. Frontend builds had crept past 10 minutes. Six of those minutes were Next.js alone, half of it stuck on "finalizing page optimization." For a team that ships multiple times a day, that kind of build time isn't a minor annoyance. It's like a very expensive tax on every single iteration. Railway’s app is overwhelmingly client-side. The dashboard is a rich, stateful interface. The canvas is real-time. Websockets are everywhere. The server-first primitives in Next.js weren't something we used, and we'd ended up building our own abstractions on top of the Pages Router just to support layouts and routing concerns that the framework didn't handle the way we needed. We were still on the Pages Router, which made shared layouts hacky. Every layout pattern was a bolted-on workaround rather than a first-class framework primitive. The App Router would have solved some of these problems, but it leans heavily into server-first patterns, and our product is intentionally client-driven. Adopting it would have meant rebuilding around a paradigm we don't need. Why TanStack Start + Vite We wanted a stack that matches how we actually build: explicit, client-first, and fast to iterate on. It also helps that we genuinely enjoy working with it. For the Product team, we wanted a few niceties that help us avoid thinking about how we needed to implement our front-end and found the following to really convince us. Type-safe routing out of the box. Route params and search params are inferred, autocomplete works across the entire route tree, and the routes themselves are generated from the file system. First-class layouts. Pathless layout routes replaced all of our previous hacks with something composable and predictable. A dev loop fast enough that you stop thinking about it. Instant HMR, near-zero startup time. The feedback cycle between changing code and seeing the result effectively disappears. SSR where it actually matters. Marketing pages, the changelog, careers. Pure client-side everywhere else, because we're not going to force server rendering on screens that don't benefit from it. (Ed: This Blog, ironically isn’t on Tanstack yet.) An explicit model. Meaning, we found TanStack to lean less on framework magic, more control over how things actually work under the hood. Several of us tried TanStack Start over the holidays and the reaction was unanimous. We like building with it, and for a product like Railway's dashboard, that matters as much as any benchmark. Two PRs, zero downtime Once we made the choice, I got to work. Pre-squash before merge, I must have made 100s of commits. Migrating a production frontend that serves millions of users across 200+ routes is the kind of thing that usually takes months of parallel running and incremental cutover. We were on a deadline, so I did it in two pull requests. PR 1 replaced everything Next.js-specific: next/image , next/head , next/router . Each was swapped for either a native browser API or a framework-agnostic alternative. This PR changed nothing about the framework itself. It just removed every dependency on it, so that PR 2 could be a clean swap. PR 2 swapped the framework. 200+ routes migrated. We systematically extracted everything non-routing-related from page files into individual React components first, then generated all routes from the original page tree. We then added Nitro as the server layer and replaced next.config.js with Nitro config, consolidating redirects (500+), security headers, and caching rules into one place. We also replaced Node.js APIs that Next.js had provided polyfills for (Buffer, url.parse , and others) with browser-native alternatives, which left us with cleaner code as a side effect. Merged on an early Sunday morning. The team dogfooded immediately with a live war room in Discord, and a stream of fixes landed same day. No downtime. What we gave up Sure we gained a faster, more explicit stack, but not without trade-offs. Built-in image optimization. We replaced next/image with <img> tags and Fastly image optimization at the edge. Parts of the ecosystem. We replaced tools like next-seo and next-sitemap with small in-house equivalents. Straightforward to build, no extra dependencies. Maturity. TanStack Start is new, and rougher edges can be expected. We're comfortable with that because the direction is right, the maintainers are responsive, and we sponsor both Vite and TanStack because we believe in where they're going. Railway's frontend runs on Railway We run our production frontend the same way our users run theirs: preview deploys per PR, health checks, zero-downtime rollouts. When we swapped the entire build system and framework, we didn't touch infrastructure. We changed code, pushed it, and Railway handled the rest. Fastly now serves most of our traffic directly from the edge. Marketing pages are cached, dynamic pages use ISR where needed, and our frontend servers are mostly idle as a result. Vite's asset model makes this work particularly well. Each module gets its own content-hashed chunk, so shipping a change to billing only invalidates that chunk. Returning users download kilobytes, not megabytes. This is how we think frontends should be deployed: the build is fast, the assets are immutable and cache-friendly, and the infrastructure underneath handles rollouts, previews, and routing without you having to think about it. Your frontend framework should be optimized for iteration speed, and your infrastructure should make shipping those iterations invisible. That's the experience we're building for ourselves and for everyone on Railway. Why now The speed of iteration on a frontend matters more now than it ever has. Builds that took 10+ minutes now finish in under two. The dev server starts instantly. Route changes are type-checked at the boundary. Layouts compose without workarounds. The gap between writing code and getting it in front of users is the bottleneck, and everything we've done here, the framework swap, the edge caching, the asset model, is about closing that gap. Vite + TanStack sets us up for a world where shipping frontend changes is near-instant, and that's the world we're building toward.