refactor: 프론트·API 를 같은 도메인(/api)으로 통합
브라우저가 프론트와 API 를 서로 다른 도메인으로 호출하던 구조를 같은 도메인의 /api 경로로 통합한다. 운영은 Nginx Proxy Manager 의 Proxy Host(/ → frontend:8080)와 Custom location(/api → backend:3000)이, 개발은 vite dev 프록시가 같은 규칙으로 백엔드에 넘긴다. - 같은 출처 호출이라 CORS 설정이 필요 없고 CSP connect-src 'self' 가 그대로 동작한다 (기존 예시대로 별도 도메인에 배포하면 CSP 에 막혀 API 호출이 실패했다) - 프론트 번들의 baseURL 이 /api 상대경로라 도메인이 바뀌어도 다시 빌드할 필요가 없다 - NPM 설정 절차와 주의점(Cache Assets 끄기, proxy 네트워크 생성)을 README "운영 배포"에 정리했다 - 프록시 설정이 빠지면 frontend nginx 가 /api 를 404 로 막는다 — index.html(200)로 조용히 실패하지 않게 함께 고친 CORS 문제: - CORS_ORIGIN 에 콤마로 여러 오리진을 적으면 문자열 전체를 하나로 비교해 모두 막히던 버그 - 운영에서도 localhost:5173·localhost:3000 이 credentials 포함으로 항상 허용되던 문제 - 형식이 잘못된 오리진(경로 포함 등)은 기동 시점에 실패하도록 검증 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,8 @@ cd frontend && npm install && npm run dev
|
||||
기동 후:
|
||||
|
||||
- 웹: <http://localhost:5173>
|
||||
- API: <http://localhost:3000/api>
|
||||
- API: <http://localhost:5173/api> — 브라우저가 쓰는 경로 (vite 프록시가 백엔드로 넘긴다, 운영과 같은 구조)
|
||||
· 백엔드 직접: <http://localhost:3000/api>
|
||||
- Swagger 문서: <http://localhost:3000/api-docs>
|
||||
|
||||
> **개발 환경에서 프론트엔드는 컨테이너로 뜨지 않습니다.**
|
||||
@@ -67,10 +68,76 @@ cd frontend && npm install && npm run dev
|
||||
> **포트는 루트 `.env.{env}` 를 따릅니다.** 호스트에서 띄우는 vite 도 `FRONTEND_PORT` 로 열리고,
|
||||
> `/api` 프록시 대상은 `BACKEND_PORT` 에서 나옵니다 — `vite.config.ts` 가 루트 env 를 직접 읽습니다.
|
||||
> 포트가 이미 사용 중이면 조용히 다른 포트로 옮기지 않고 **실패**합니다
|
||||
> (`CORS_ORIGIN` 과 compose 포트 매핑이 그 값에 묶여 있어, 말없이 옮겨가면 원인 모르게 어긋납니다).
|
||||
> (대개 다른 vite 가 이미 그 포트에 떠 있다는 뜻이라, 말없이 옮겨가면 브라우저가 엉뚱한 서버를 보게 됩니다).
|
||||
|
||||
전체 실행/배포 명령어는 [`.claude/skills/docker/SKILL.md`](./.claude/skills/docker/SKILL.md) 참조.
|
||||
|
||||
## 운영 배포 — 프론트·API 를 같은 도메인으로 (Nginx Proxy Manager)
|
||||
|
||||
브라우저는 **도메인 하나만** 봅니다. 경로로 프론트와 백엔드를 나눕니다.
|
||||
|
||||
```
|
||||
https://app.your-domain.com/ → <PROJECT_NAME>-frontend:8080 nginx 정적 호스팅 (Proxy Host 기본 대상)
|
||||
https://app.your-domain.com/api/... → <PROJECT_NAME>-backend:3000 NestJS (Custom location /api)
|
||||
```
|
||||
|
||||
- 프론트 번들은 API 를 `/api` **상대경로**로 호출합니다 — 도메인이 바뀌어도 다시 빌드할 필요가 없습니다.
|
||||
- 같은 출처 요청이라 **CORS 설정이 필요 없고**, CSP `connect-src 'self'` 가 그대로 동작하며, 인증 쿠키도 같은 사이트로 다룹니다.
|
||||
- 개발 환경도 같은 구조입니다 — vite dev 서버가 `/api` 를 백엔드로 프록시합니다.
|
||||
|
||||
### 1) 공유 네트워크 (서버당 1회)
|
||||
|
||||
NPM 과 앱 컨테이너가 외부 네트워크 `proxy` 를 공유해야 NPM 이 컨테이너 이름으로 찾아갈 수 있습니다.
|
||||
이 네트워크가 없으면 compose 가 기동하지 않습니다.
|
||||
|
||||
```bash
|
||||
docker network create proxy # 이미 있으면 생략
|
||||
docker network connect proxy <NPM 컨테이너 이름> # NPM 이 아직 proxy 네트워크에 없다면
|
||||
```
|
||||
|
||||
> NPM 은 `<PROJECT_NAME>-frontend` 같은 **컨테이너 이름**으로 접근하므로, 같은 서버에 여러 프로젝트를 올린다면 `PROJECT_NAME` 이 겹치지 않게 합니다.
|
||||
|
||||
### 2) 앱 기동
|
||||
|
||||
```bash
|
||||
cp .env.production.example .env.production # placeholder 교체 — BACKEND_API_URL=/api 는 그대로 둔다
|
||||
cp backend/.env.production.example backend/.env.production
|
||||
sudo docker compose --env-file .env.production up -d --build
|
||||
```
|
||||
|
||||
### 3) NPM 설정 — Proxy Host 하나 + Custom location 하나
|
||||
|
||||
| 탭 | 항목 | 값 |
|
||||
| --- | --- | --- |
|
||||
| Details | Domain Names | `app.your-domain.com` |
|
||||
| | Scheme · Forward Hostname · Port | `http` · `<PROJECT_NAME>-frontend` · `8080` |
|
||||
| | Cache Assets | **끔** (아래 주의 참조) |
|
||||
| | Block Common Exploits | 켬 (선택) |
|
||||
| Custom locations | location | `/api` |
|
||||
| | Scheme · Forward Hostname · Port | `http` · `<PROJECT_NAME>-backend` · `3000` (`BACKEND_PORT`) |
|
||||
| SSL | | 인증서 발급 + Force SSL |
|
||||
|
||||
> ⚠️ **Cache Assets 는 끕니다.** 켜면 NPM 이 `.png`·`.js`·`.css` 등으로 끝나는 요청을 custom location 안에서도 가로채
|
||||
> **Proxy Host 의 기본 대상(frontend)** 으로 보냅니다. `/api/files/abc.png` 같은 API 가 프론트로 가 버립니다.
|
||||
> 프론트 정적 자원의 캐시 헤더는 frontend nginx 가 이미 붙입니다.
|
||||
|
||||
> - location 은 `/api` (끝 슬래시 없음) — 접두어 매칭이라 Swagger(`/api-docs`)까지 백엔드로 갑니다. 개발의 vite 프록시와 같은 규칙입니다.
|
||||
> - Forward Hostname 에 경로를 붙이지 않습니다. 요청 경로가 그대로 넘어가야 백엔드의 `/api` prefix 와 맞습니다.
|
||||
> - **`/api` 로 시작하는 경로는 프론트 라우트로 쓰지 않습니다** (전부 백엔드로 갑니다).
|
||||
|
||||
### 4) 확인
|
||||
|
||||
```bash
|
||||
curl -i https://app.your-domain.com/api/health # 200 {"status":"ok",...} ← 백엔드가 응답
|
||||
curl -I https://app.your-domain.com/ # 200 text/html ← 프론트가 응답
|
||||
```
|
||||
|
||||
`/api/health` 가 **404** 면 custom location 이 빠진 것입니다 —
|
||||
frontend nginx 는 자기에게 떨어진 `/api` 요청을 404 로 막아 둡니다 (index.html 로 폴백해 조용히 실패하지 않도록).
|
||||
|
||||
> API 를 별도 도메인(`api.your-domain.com`)에 두는 구성이 필요하다면 `.env.production.example` 의 안내대로
|
||||
> `BACKEND_API_URL`·`CORS_ORIGIN` 과 `frontend/nginx.conf` 의 CSP `connect-src` 를 함께 바꿉니다.
|
||||
|
||||
## 모바일 앱 실행 (Flutter)
|
||||
|
||||
Flutter 는 Docker Compose 대상이 아니므로 로컬에서 직접 실행합니다.
|
||||
@@ -79,7 +146,8 @@ Flutter 는 Docker Compose 대상이 아니므로 로컬에서 직접 실행합
|
||||
cd flutter && cp .env.example .env && flutter pub get && flutter run
|
||||
```
|
||||
|
||||
> `flutter/.env` 의 `API_URL` 을 백엔드 주소로 설정합니다. (Android 에뮬레이터는 `http://10.0.2.2:3000/api`)
|
||||
> `flutter/.env` 의 `API_URL` 을 백엔드 주소로 설정합니다. (Android 에뮬레이터는 `http://10.0.2.2:3000/api`,
|
||||
> 운영은 웹과 같은 도메인의 `https://app.your-domain.com/api`)
|
||||
|
||||
## 환경변수
|
||||
|
||||
@@ -92,7 +160,7 @@ cd flutter && cp .env.example .env && flutter pub get && flutter run
|
||||
| ----------------------------- | ---------------------------------------- | --------------------- |
|
||||
| `.env.{env}.example` | 루트 — 모든 설정의 SSOT (compose 주입) | ✅ 여기에 작성 |
|
||||
| `backend/.env.{env}.example` | 백엔드 — 고유 시크릿(JWT 등)만 | 최소 (존재 필수) |
|
||||
| `frontend/.env.{env}.example` | 프론트 — 빌드 시 루트 `BACKEND_API_URL` 주입 | 작성 불필요 |
|
||||
| `frontend/.env.{env}.example` | 프론트 — 빌드 시 루트 `BACKEND_API_URL`(기본 `/api`) 주입 | 작성 불필요 |
|
||||
| `flutter/.env.example` | 모바일 — `API_URL` (compose 비대상) | ✅ 독립 관리 |
|
||||
|
||||
`{env}` = `development` | `production`
|
||||
|
||||
Reference in New Issue
Block a user