문제 상황
하나의 EC2 인스턴스에서 여러 개의 Vite 앱을 서브 경로로 서빙하려고 했음:
- 메인 앱:
/ - 서비스 A:
/service-a/ - 서비스 B:
/service-b/ - 관리자 페이지:
/admin/
하지만 다음과 같은 문제들이 발생함:
- Nginx Permission Denied (500 에러)
- 서브 경로 404/500 에러
- CORS 정책 오류
- SPA 라우팅 문제 (새로고침 시 404)
문제 1: Nginx Permission Denied
오류 메시지
1
2
| [crit] stat() "/home/ubuntu/app/dist/index.html" failed (13: Permission denied)
[error] rewrite or internal redirection cycle while internally redirecting to "/index.html"
|
원인
- Nginx가
www-data 사용자로 실행되는데, 파일은 ubuntu 사용자 소유 dist 디렉토리나 파일에 읽기 권한이 없음
해결 방법
1. Nginx 사용자 변경
1
2
3
4
5
6
7
8
| # Nginx 설정 수정
sudo vi /etc/nginx/nginx.conf
# user 지시어를 ubuntu로 변경
user ubuntu;
# Nginx 재시작
sudo systemctl restart nginx
|
2. 파일 권한 설정
1
2
3
4
5
| # dist 디렉토리 권한 설정
sudo chmod -R 755 /home/ubuntu/app/*/dist
# 소유권 설정
sudo chown -R ubuntu:ubuntu /home/ubuntu/app
|
문제 2: 서브 경로 404/500 에러
증상
- 메인 앱(
/)은 정상 작동 /service-a, /service-b 등 서브 경로에서 404 또는 500 에러 발생- 정적 파일(JS, CSS)을 찾을 수 없음
원인
- Vite
base 경로 미설정: 빌드 시 상대 경로로 빌드되어 서브 디렉토리에서 정적 파일을 찾지 못함 - Nginx
alias 설정 오류: 경로 매핑이 올바르지 않음
해결 방법
1. Vite 설정에 base 추가
각 프로젝트의 vite.config.js 또는 vite.config.ts에 base 경로 설정:
1
2
3
4
5
| // vite.config.ts (Service A)
export default defineConfig({
base: '/service-a/',
plugins: [react()],
})
|
1
2
3
4
5
| // vite.config.ts (Service B)
export default defineConfig({
base: '/service-b/',
plugins: [react()],
})
|
1
2
3
4
5
| // vite.config.ts (Admin)
export default defineConfig({
base: '/admin/',
plugins: [react()],
})
|
2. Nginx 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # Service A
location = /service-a {
return 301 /service-a/;
}
location /service-a/ {
alias /home/ubuntu/app/service-a/dist/;
try_files $uri $uri/ /service-a/index.html;
index index.html;
}
# Service B
location = /service-b {
return 301 /service-b/;
}
location /service-b/ {
alias /home/ubuntu/app/service-b/dist/;
try_files $uri $uri/ /service-b/index.html;
index index.html;
}
# Admin
location = /admin {
return 301 /admin/;
}
location /admin/ {
alias /home/ubuntu/app/admin/dist/;
try_files $uri $uri/ /admin/index.html;
index index.html;
}
|
주의:
- Vite의
base는 끝에 슬래시를 포함해야 함 ('/service-a/') - Nginx의
alias도 끝에 슬래시를 포함해야 함 location = /service-a로 trailing slash 없는 요청을 리다이렉트
3. 프로젝트 재빌드
1
2
3
4
5
6
7
8
| cd ~/app/service-a
npm run build
cd ~/app/service-b
npm run build
cd ~/app/admin
npm run build
|
문제 3: CORS 정책 오류
오류 메시지
1
2
3
| Access to fetch at 'http://localhost:3000/api/data' from origin 'http://3.36.65.229'
has been blocked by CORS policy: The request client is not a secure context
and the resource is in more-private address space `loopback`.
|
원인
프론트엔드 코드에서 개발 환경용 localhost URL이 하드코딩되어 있어, 프로덕션에서도 localhost로 요청을 시도함.
해결 방법
1. API 클라이언트 수정
1
2
3
| // src/utils/apiClient.js (Service A)
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ||
(import.meta.env.PROD ? '/api/service-a/' : 'http://localhost:3000/api');
|
1
2
3
| // src/utils/apiClient.js (Service B)
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ||
(import.meta.env.PROD ? '/api/service-b/' : 'http://localhost:3001/api');
|
2. Nginx API 프록시 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # Service A API 프록시
location /api/service-a/ {
proxy_pass http://localhost:3000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Service B API 프록시
location /api/service-b/ {
proxy_pass http://localhost:3001/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
|
참고: import.meta.env.PROD는 Vite가 빌드 시 자동으로 설정하는 환경 변수
문제 4: SPA 라우팅 문제
증상
- 직접 URL로 접근하면 404 에러 발생
- 새로고침하면 페이지가 나타남
- 브라우저 뒤로가기/앞으로가기가 제대로 작동하지 않음
원인
- React Router basename 미설정: Vite의
base 설정과 React Router의 basename이 일치하지 않음 - Nginx fallback 설정 오류: SPA 라우팅을 위한 fallback이 제대로 작동하지 않음
해결 방법
1. React Router basename 설정
1
2
3
4
5
6
7
8
9
10
| // src/main.jsx (Service A)
import { BrowserRouter } from 'react-router-dom'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter basename="/service-a">
<App />
</BrowserRouter>
</React.StrictMode>,
)
|
1
2
3
4
| // src/main.jsx (Service B)
<BrowserRouter basename="/service-b">
<App />
</BrowserRouter>
|
1
2
3
4
| // src/main.jsx (Admin)
<BrowserRouter basename="/admin">
<App />
</BrowserRouter>
|
2. Nginx fallback 확인
1
2
3
4
5
| location /service-a/ {
alias /home/ubuntu/app/service-a/dist/;
try_files $uri $uri/ /service-a/index.html; # ← fallback 필수
index index.html;
}
|
중요: basename은 React Router가 URL을 해석할 때 사용하는 기본 경로이므로, Vite의 base 설정과 반드시 일치해야 함.
결과
Before
- Nginx Permission Denied로 500 에러
- 서브 경로에서 정적 파일을 찾지 못해 404 에러
- CORS 정책 오류로 API 요청 실패
- SPA 라우팅이 작동하지 않아 새로고침 시 404
After
- 모든 경로에서 정상적으로 앱 서빙
- API 요청이 Nginx 프록시를 통해 정상 작동
- SPA 라우팅이 완벽하게 동작
- 브라우저 뒤로가기/앞으로가기 정상 작동
아키텍처 개요
1
2
3
4
5
6
7
8
9
10
| [사용자] --HTTP:80--> [Nginx] --라우팅--> [정적 파일 or 백엔드 API]
|
+-> / → main-app/dist
+-> /service-a/ → service-a/dist
+-> /service-b/ → service-b/dist
+-> /admin/ → admin/dist
+-> /api/service-a/ → localhost:3000 (Service A Backend)
+-> /api/service-b/ → localhost:3001 (Service B Backend)
[PM2] --관리--> [Service A Backend:3000, Service B Backend:3001]
|
주의할 점
1. base 경로는 반드시 슬래시로 끝나야 함
1
2
3
4
5
| // ❌ 잘못된 예
base: '/service-a'
// ✅ 올바른 예
base: '/service-a/'
|
2. Vite base와 React Router basename은 일치해야 함
1
2
3
4
5
| // vite.config.ts
base: '/service-a/'
// main.jsx
<BrowserRouter basename="/service-a">
|
3. Nginx alias도 슬래시로 끝나야 함
1
2
3
4
5
| # ❌ 잘못된 예
alias /home/ubuntu/app/service-a/dist;
# ✅ 올바른 예
alias /home/ubuntu/app/service-a/dist/;
|
4. PM2로 백엔드 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # pm2.config.js
module.exports = {
apps: [
{
name: 'service-a-backend',
cwd: '/home/ubuntu/app/service-a/backend',
script: 'npm',
args: 'start',
env: {
NODE_ENV: 'production',
PORT: 3000
}
},
{
name: 'service-b-backend',
cwd: '/home/ubuntu/app/service-b/backend',
script: 'npm',
args: 'start',
env: {
NODE_ENV: 'production',
PORT: 3001
}
}
]
};
# PM2 시작
pm2 start pm2.config.js
pm2 save
|
5. 프론트엔드 업데이트 후
1
2
3
4
5
6
7
8
9
| # 프론트엔드 빌드
npm run build
# Nginx 완전 재시작
sudo systemctl stop nginx
sudo pkill -9 nginx
sudo systemctl start nginx
# 브라우저 하드 리프레시 (Ctrl + Shift + R)
|
트러블슈팅 체크리스트
- Nginx 사용자가
ubuntu로 설정되었는가? dist 폴더 권한이 755인가?- Vite
base 경로가 설정되었는가? - React Router
basename이 base와 일치하는가? - Nginx
alias가 올바르게 설정되었는가? try_files에 fallback이 설정되었는가?- API 요청이 프로덕션 환경에서는 상대 경로를 사용하는가?
- PM2로 백엔드가 정상 실행 중인가?
EC2에서 여러 Vite 앱을 서브 경로로 서빙하려면 Vite base, React Router basename, Nginx alias를 모두 일치시켜야 함. 특히 CORS 문제를 해결하기 위해 API 요청을 Nginx 프록시를 통하도록 설정하는 것이 핵심임.
도움이 되셨길 바랍니다! 😀