2026. 6. 28.
홈은 되는데 /dashboard만 404, Vercel SPA rewrite부터 확인하세요
AI가 만든 React/Vite 앱을 Vercel에 올린 뒤 홈은 열리는데 /dashboard 같은 하위 주소를 새로고침할 때만 404가 난다면, 라우터 전체를 갈아엎기 전에 SPA rewrites와 React Router preset을 3분 순서로 확인해야 합니다.

AI 코딩 도구로 React 앱을 만들고 Vercel에 배포했는데 홈 화면은 잘 열립니다. 메뉴를 눌러 /dashboard로 이동해도 화면이 보입니다. 그런데 주소창에서 새로고침하거나 링크를 직접 열면 갑자기 404가 나옵니다. 이때 초보자는 라우터 코드를 전부 다시 짜야 한다고 느끼기 쉽습니다.
하지만 이 증상은 라우터 로직보다 배포 서버가 하위 주소를 어떤 파일로 보내는지에서 먼저 확인해야 합니다. 홈은 정상인데 하위 경로 새로고침에서만 404라면, React 컴포넌트보다 Vercel의 SPA fallback 설정을 먼저 보는 편이 빠릅니다.
vercel.com의 Vite 문서는 Vite 앱을 SPA로 배포하면 deep linking이 기본으로 동작하지 않을 수 있고, 이 경우 프로젝트 루트의 vercel.json에 /(.*) 요청을 /index.html로 보내는 rewrites를 추가하라고 안내합니다. reactrouter.com의 SPA 문서도 유효한 앱 경로에서 404가 난다면 호스트가 모든 URL을 index.html로 보내도록 설정해야 할 가능성이 높다고 설명합니다. 다만 모든 React Router 프로젝트에 같은 처방을 넣으면 안 됩니다. vercel.com의 React Router 문서는 React Router를 프레임워크로 쓸 때 Vercel Preset 사용을 권장합니다.
비슷한 배포 실패를 로그부터 좁히고 싶다면 Vercel 배포 실패에서 멈춘 지점을 찾는 순서를 먼저 보면 좋습니다. GitHub Actions에서 같은 빌드가 깨진다면 GitHub Actions 실패 로그를 먼저 읽는 방법처럼 실패 지점을 한 줄로 줄인 뒤 이 글의 라우팅 설정을 적용합니다.
30초 안에 증상을 먼저 좁힙니다
먼저 문제가 정말 SPA fallback 문제인지 확인합니다. 아래 세 가지가 같이 맞으면 가능성이 높습니다.
홈 주소: https://내앱.vercel.app/ 정상
앱 안에서 이동: /dashboard 정상
주소창 새로고침 또는 직접 접속: /dashboard 404
반대로 앱 안에서 메뉴를 눌러도 /dashboard가 안 열리면 라우트 정의, 링크 주소, 컴포넌트 import 문제일 수 있습니다. 홈 주소부터 404라면 빌드 출력 폴더나 Vercel 프로젝트 설정 문제일 수 있습니다. 이 글의 범위는 홈은 되는데 직접 주소만 404인 경우입니다.
초보자가 여기서 가장 많이 하는 실수는 AI에게 "React Router 고쳐줘"라고 크게 요청하는 것입니다. 그러면 AI가 라우트 파일, 레이아웃, 상태 관리까지 건드릴 수 있습니다. 먼저 실패 조건을 작게 적어야 합니다.
Vercel 배포 후 홈(/)은 정상입니다.
앱 내부 링크로 /dashboard 이동도 정상입니다.
하지만 주소창에서 /dashboard를 새로고침하거나 직접 열면 404가 납니다.
React 컴포넌트 전체를 바꾸지 말고 배포 라우팅 설정부터 원인을 좁혀 주세요.
이렇게 말하면 수정 범위가 라우터 전체에서 배포 설정으로 좁아집니다.
1분째에는 프로젝트 종류를 구분합니다
여기서 바로 vercel.json을 넣기 전에 프로젝트가 어떤 종류인지 봅니다. 이름이 모두 React Router여도 배포 방식이 다를 수 있습니다.
일반적인 React/Vite SPA는 보통 vite, @vitejs/plugin-react, react-router-dom 조합으로 만들어집니다. npm run build를 실행하면 dist 폴더와 index.html이 생기는 구조입니다. 이런 프로젝트는 브라우저가 처음 받은 index.html 안에서 React Router가 /dashboard 화면을 그립니다.
반면 최신 React Router를 프레임워크 모드로 쓰는 프로젝트는 @react-router/dev, react-router.config.ts, routes.ts 같은 파일이 보일 수 있습니다. Vercel 문서는 이 경우 @vercel/react-router의 Vercel Preset 사용을 권장합니다. 이 프로젝트에 무작정 catch-all rewrite를 넣으면 프레임워크가 제공하는 서버 렌더링이나 라우팅 동작과 충돌할 수 있습니다.
아래처럼 먼저 파일을 봅니다.
일반 Vite SPA 쪽 단서:
- vite.config.ts 또는 vite.config.js
- src/main.tsx
- react-router-dom
- build 결과 dist/index.html
React Router 프레임워크 쪽 단서:
- react-router.config.ts
- app/routes.ts 또는 app/routes 폴더
- @react-router/dev
- @vercel/react-router preset 설정 가능
판단이 어렵다면 AI에게 이렇게 묻습니다.
이 프로젝트가 일반 React/Vite SPA인지,
React Router 프레임워크 모드인지 package.json과 설정 파일 기준으로 구분해 주세요.
구분 전에는 vercel.json rewrites를 추가하지 마세요.
일반 React/Vite SPA로 확인된 뒤에만 다음 단계의 rewrites를 적용하는 것이 안전합니다.
2분째에는 vercel.json rewrites를 추가합니다
일반 Vite SPA라면 프로젝트 루트에 vercel.json을 만들고 아래 설정을 넣습니다. package.json과 같은 위치입니다.
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
이 설정의 뜻은 간단합니다. /dashboard, /login, /posts/1 같은 요청이 들어오면 Vercel이 브라우저 주소를 바꾸지 않고 /index.html을 반환합니다. 그 다음 React Router가 브라우저 안에서 실제 화면을 고릅니다. Vercel rewrites 문서의 설명처럼 rewrite는 브라우저 URL을 바꾸지 않고 다른 destination으로 요청을 보냅니다.
이미 vercel.json이 있다면 덮어쓰지 말고 기존 설정에 rewrites만 합칩니다.
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
],
"headers": []
}
여기서 cleanUrls를 쓰고 있다면 주의합니다. Vercel의 Vite 문서는 cleanUrls가 true일 때 파일 확장자를 source나 destination에 포함하지 말라고 안내합니다. 이 경우 /index.html 대신 /로 맞춰야 할 수 있습니다.
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"cleanUrls": true,
"rewrites": [
{
"source": "/(.*)",
"destination": "/"
}
]
}
초보 단계에서는 먼저 cleanUrls가 없는 기본 설정으로 해결하는 편이 낫습니다. 이미 프로젝트가 cleanUrls를 쓰고 있을 때만 두 번째 형태를 검토합니다.
3분째에는 build 출력 폴더와 index.html을 확인합니다
rewrites를 추가했는데도 404가 남으면 index.html이 실제 배포 출력 폴더에 있는지 확인합니다. Vite 기본값은 보통 dist입니다.
npm run build
실행 후 아래 파일이 있는지 봅니다.
dist/index.html
dist/assets/...
Vercel 프로젝트 설정의 Output Directory가 dist와 맞지 않으면 Vercel은 올바른 index.html을 찾지 못할 수 있습니다. 이때는 코드보다 배포 설정을 먼저 맞춥니다.
Build Command: npm run build
Output Directory: dist
Install Command: npm install
AI에게는 이렇게 요청합니다.
npm run build 결과 dist/index.html이 생성되는지 확인해 주세요.
Vercel Output Directory가 dist와 맞는지도 확인해 주세요.
코드 구조를 바꾸지 말고 배포 설정 불일치만 찾아 주세요.
이 문장에는 두 가지 제한이 들어 있습니다. 코드 구조를 바꾸지 말 것, 배포 설정 불일치만 찾을 것입니다. 이 제한이 없으면 AI가 라우터를 HashRouter로 바꾸거나 폴더 구조를 크게 바꾸는 제안을 할 수 있습니다.
HashRouter로 바꾸기 전에 한 번 멈춥니다
인터넷 검색이나 AI 답변에서 HashRouter로 바꾸라는 조언을 볼 수 있습니다. 주소가 /#/dashboard처럼 바뀌기 때문에 새로고침 404를 피할 수는 있습니다. 하지만 이미 /dashboard 같은 깔끔한 URL을 쓰고 있다면 먼저 서버 fallback을 맞추는 편이 자연스럽습니다.
HashRouter는 임시 회피책으로는 쓸 수 있지만, 공유 URL 모양과 라우팅 전략이 바뀝니다. 팀 프로젝트나 포트폴리오 링크를 생각하면 <u>URL 형태를 바꾸는 수정은 마지막 선택</u>으로 남기는 편이 좋습니다.
먼저 아래 순서를 지킵니다.
1. 홈은 정상이고 새로고침만 404인지 확인
2. 일반 React/Vite SPA인지 확인
3. SPA면 vercel.json rewrites 추가
4. dist/index.html과 Output Directory 확인
5. 그래도 안 되면 라우터 전략을 다시 검토
이 순서대로 보면 라우터 전체를 다시 만들 가능성이 줄어듭니다.
AI에게 붙여 넣을 7줄 프롬프트
아래 프롬프트를 그대로 채워 넣으면 AI가 문제를 넓게 고치지 않고 필요한 파일만 보게 만들 수 있습니다.
React/Vite 앱을 Vercel에 배포했습니다.
홈 주소(/)는 정상이고 앱 내부 링크로 /dashboard 이동도 정상입니다.
하지만 /dashboard를 주소창에서 새로고침하거나 직접 접속하면 404가 납니다.
이 프로젝트가 일반 Vite SPA인지 React Router 프레임워크 모드인지 먼저 구분해 주세요.
일반 Vite SPA라면 vercel.json rewrites 설정을 제안해 주세요.
React Router 프레임워크 모드라면 Vercel Preset 설정 여부를 먼저 확인해 주세요.
라우터 컴포넌트 전체를 다시 작성하지 말고 최소 수정만 제안해 주세요.
이 프롬프트의 핵심은 "먼저 구분"입니다. 같은 404라도 일반 Vite SPA와 React Router 프레임워크 모드는 처방이 다릅니다. 무조건 rewrite를 넣는 것이 아니라, 현재 프로젝트가 어떤 배포 방식을 쓰는지 먼저 보는 것이 손실을 줄입니다.
마지막으로 지금 할 일은 하나입니다. Vercel에서 404가 나는 주소를 하나 고르고, 홈, 내부 이동, 새로고침 결과를 나눠 적습니다. 그 다음 프로젝트 파일에서 Vite SPA인지 React Router 프레임워크 모드인지 확인합니다. 일반 Vite SPA라면 vercel.json rewrites를 추가하고 다시 배포합니다. 이 세 단계만 지켜도 "배포가 망했다"는 느낌 대신, 고칠 지점이 훨씬 작아집니다.
자주 묻는 질문
| 질문 | 답 |
|---|---|
Vercel 새로고침 404는 항상 vercel.json으로 고치나요? | 아닙니다. 일반 Vite SPA인지 React Router 프레임워크 모드인지 먼저 구분해야 합니다. |
| HashRouter로 바꾸면 더 빠르지 않나요? | 임시 회피는 될 수 있지만 URL 형태가 바뀌므로 서버 fallback과 배포 설정을 먼저 확인하는 편이 안전합니다. |
| Output Directory는 왜 확인하나요? | rewrites가 맞아도 배포 결과물 안에 index.html이 없거나 출력 폴더가 다르면 하위 경로 요청을 받을 파일이 없습니다. |
참고 출처
다음으로 읽을 기사
같은 흐름으로 이어 읽기 좋은 기사만 추려 보여줍니다.
첫 번째 댓글을 남겨보세요
여러분의 생각이 다른 독자에게 도움이 됩니다.
댓글 0
이 글을 읽은 독자들의 생각을 나눠보세요.