TL;DR
-
React Router v7의 prerender 기능이 Netlify 환경에서 "Server build file not found" 에러로 실패
-
원인: Netlify 플러그인이 rollupOptions.input을 덮어써서 server-build가 manifest에서 제외됨
-
해결: enforce: 'post' 플러그인으로 우회 후, Netlify 플러그인에 PR 기여 → v2.1.3에 반영됨
배경: Pre-rendering(SSG)란?
빌드 시점에 특정 라우트를 미리 HTML로 생성해두는 것.
요청 → 이미 생성된 HTML 반환 (SSR 불필요, CDN에서 바로 서빙)문제 분석
React Router의 prerender는 단순한 케이스를 위해 설계됨:
[일반적인 구조]
vite build → server-build 생성 → prerender가 server-build 로드 → HTML 생성하지만 커스텀 서버 (Vercel Functions, Netlify Functions 등)를 쓰면:
[커스텀 서버 구조]
vite build → server-build 생성
→ custom server entry 생성
prerender는 server-build만 로드 → custom server entry 무시됨!이로 인해 React Router v7의 prerender 기능이 Netlify, Cloudflare, Vercel 등 3rd party 서버리스 플랫폼 플러그인과 호환되지 않음.
증상
[react-router] Server build file not found in manifest원인
플랫폼 플러그인들이 서버 빌드 출력을 수정하여 React Router의 prerender 프로세스가 manifest에서 서버 빌드 파일을 찾지 못함.
Netlify 플러그인의 경우:@netlify/vite-plugin-react-router가 config 훅에서 rollupOptions.input을 덮어쓰기함:
// node_modules/@netlify/vite-plugin-react-router/dist/index.js
config.build.rollupOptions.input = {
server: "virtual:netlify-server"
};이로 인해 virtual:react-router/server-build가 manifest에서 제외됨.
해결 과정
시도 1: 기존 Workaround (실패)
https://github.com/remix-run/react-router/issues/13226 에서 제시된 workaround:
export default defineConfig(({ isSsrBuild }) => ({
build: {
rollupOptions: isSsrBuild
? { input: ['virtual:react-router/server-build'] }
: undefined,
},
}));Netlify 환경에서는 플러그인이 이 설정을 덮어쓰기 때문에 동일한 에러 발생.
시도 2: enforce: 'post' 플러그인 (성공)
⚠️ v2.1.3 이후로는 이 workaround가 필요 없음enforce: 'post'를 사용해 Netlify 플러그인 이후에 server-build를 input에 추가:
// vite.config.ts
import { reactRouter } from '@react-router/dev/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import netlifyReactRouter from '@netlify/vite-plugin-react-router';
import netlify from '@netlify/vite-plugin';
export default defineConfig({
plugins: [
reactRouter(),
tsconfigPaths(),
netlifyReactRouter(),
netlify(),
// Prerender fix: server-build를 rollup input에 추가
{
name: 'add-server-build-to-input',
enforce: 'post',
config(config, { command, isSsrBuild }) {
if (isSsrBuild && command === 'build') {
const currentInput = config.build?.rollupOptions?.input;
if (currentInput && typeof currentInput === 'object') {
return {
build: {
rollupOptions: {
input: {
...currentInput,
'server-build': 'virtual:react-router/server-build',
},
},
},
};
}
}
},
},
],
});build/server/server.js 0.35 kB
build/server/assets/server-build-lv_U4l46.js 14.57 kB ← 추가됨!
Prerender (html): / -> build/client/index.html
✓ built in 292ms시도 3: 근본 해결 - PR 기여
근본적인 해결을 위해 @netlify/vite-plugin-react-router에 PR 제출:
플러그인이 기존 input을 덮어쓰지 않고 병합하도록 수정:
// Before (문제)
config.build.rollupOptions.input = {
server: "virtual:netlify-server"
};
// After (수정)
const existingInput = config.build?.rollupOptions?.input;
// ... 기존 input 형태에 따라 병합 처리
config.build.rollupOptions.input = {
...mergedInput,
server: "virtual:netlify-server",
};
향후 React Router 팀에서 Vite Preview Server 방식으로 공식 지원 예정 (RFC #14651)
후속 진행
PR #607은 GitHub Actions 워크플로우 권한 문제로 인해 외부 기여자가 직접 CI를 실행할 수 없어 닫혔다.
하지만 메인테이너(Philippe Serhal)가 PR #620을 새로 생성하면서:
-
내 커밋을 체리픽하여 포함
-
코드를 더 견고하게 개선 (rollup input 병합 로직 분리, 테스트 커버리지 추가)
-
PR 본문에 "Inspired by #606 (thank you @nahyeongjin1!)" 언급
2026년 1월 19일, PR #620이 머지되었다! 🎉
이제 workaround 없이 깔끔한 설정으로 prerender가 동작한다:

관련 자료
이슈 & PR
| 링크 | 설명 |
|---|---|
| #13226 | Cloudflare Workers 동일 이슈 |
| #14096 | Netlify 이슈 (closed as not planned) |
| Discussion #14651 | Vite Preview Server RFC |
| PR #14650 | Preview Server 구현 (리뷰 대기) |
| Netlify #606 | Netlify 플러그인 이슈 |
| Netlify #607 | Netlify 플러그인 수정 PR |
| Netlify #620 | Netlify 플러그인 수정 PR (머지됨 ✅) |