Files
project-base/README.md
T
ttipo 7897f2de20 fix: 템플릿 점검 결과 반영 — Node 24·이미지 위생·운영 노출 최소화
템플릿 점검에서 나온 항목을 한 번에 반영한다. 항목별 이유:

- Node 20 → 24 LTS (Dockerfile·CI): Node 20 은 2026-04-30 에 지원이 끝나 보안 패치가 없다
- backend·frontend .dockerignore 추가: `COPY . .` 가 호스트 node_modules(Windows 바이너리)·dist·
  .env*(시크릿)를 이미지에 넣고 있었다
- 업로드 디렉터리 쓰기 검사(fail-fast): Linux 에서 Docker 가 root 소유로 만든 바인드 마운트에
  비-root 앱이 쓰지 못해 첫 업로드에서야 SYS_001 로 터졌다. 기동 시 해결 명령을 안내한다
- DB 커넥션 풀 100 → 10 (DB_POOL_MAX): PostgreSQL 기본 max_connections(100)를 앱 하나가 다 잡았다
- Swagger 운영 기본 비활성 (SWAGGER_ENABLED): API 구조 전체가 공개 도메인에 노출됐다
- 헬스체크 운영 응답에서 오류 상세 제거: 드라이버 오류 문구에 내부 주소·DB 계정명이 담긴다.
  원인은 error 로그로 남긴다
- nginx: location 의 add_header 때문에 정적 자원에서 보안 헤더가 빠지던 문제와,
  index.html 이 캐시돼 배포 직후 빈 화면이 날 수 있던 문제를 고친다
- vite allowedHosts: true 제거 (DEV_ALLOWED_HOSTS): DNS rebinding 보호가 꺼져 있었다
- 전역 설정을 configureApp() 으로 분리해 main.ts 와 e2e 가 공유: e2e 가 설정을 복사해 재현해
  실제 설정과 어긋나도 통과할 수 있었다. PORT 도 빈 값을 방어하는 파서를 거친다
- 쓰이지 않던 WEBSOCKET_PORT 제거: 별도 포트는 같은 도메인 라우팅과 맞지 않는다
- 예제 useUser 가 메시지 대신 에러 코드를 보관하도록 수정, testing.md 표 깨짐 수정,
  settings.json 허용 목록에서 npm install 제거 (Windows lock 파일 규칙과 충돌)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 00:06:02 +09:00

15 KiB

풀스택 모노레포 기본 템플릿

NestJS(백엔드) · Vue 3(웹) · Flutter(모바일)를 하나의 저장소에서 운영하는 풀스택 템플릿입니다. 세 클라이언트는 동일한 표준 응답 포맷과 공통 에러 코드 맵을 공유하며, Docker Compose로 한 번에 구동됩니다.

구성

디렉터리 스택 역할 포트(기본)
backend/ NestJS + TypeORM REST API 서버 3000
frontend/ Vue 3 + Vite 웹 프론트엔드 5173
flutter/ Flutter + Riverpod 모바일 앱 -
db PostgreSQL 데이터베이스 5432
redis Redis 캐시 / 세션 (기본 비활성) 6379

사전 요구사항

  • Docker / Docker Compose — backend·frontend·db·redis 구동 (컨테이너는 Node 24 LTS 이미지를 쓴다)
  • Node.js 24 LTS 권장 (최소 22.12) — 호스트에서 띄우는 vite dev 서버와 lint·test 용
  • Flutter SDK ^3.11 — 모바일 앱 실행용 (compose 대상 아님)

빠른 시작 (Docker)

# 1) 환경변수 파일 생성 (예제 복사)
#    실제 설정값은 루트 .env.development 한 곳에서 관리한다 (SSOT).
#    backend/frontend 는 docker-compose 가 값을 주입하므로 예제만 복사하면 된다.
cp .env.development.example .env.development                       # ← 여기에만 값 입력
cp backend/.env.development.example backend/.env.development       # (존재 필요: JWT 등 고유 시크릿)
# frontend 는 compose 가 빌드 시 값을 주입하므로 자체 env 복사 불필요

# 2) 백엔드 스택 기동 (backend + db)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d

# 3) 의존성 변경 시에만 --build
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d --build

# 4) 프론트엔드는 호스트에서 직접 띄운다 (컨테이너보다 수십 배 빠르다)
cd frontend && npm install && npm run dev

기동 후:

개발 환경에서 프론트엔드는 컨테이너로 뜨지 않습니다. vite 를 컨테이너에서 돌리면 소스가 Windows 바인드 마운트 너머에 있어 첫 서빙까지 수 분이 걸립니다. 호스트에서 npm run dev 로 띄우면 1~2초이고, /api 요청은 vite 프록시가 백엔드 컨테이너로 넘깁니다. 컨테이너로 띄워야 한다면 --profile frontend 를 붙이세요 (아래 "프론트엔드 실행 방식" 참조). 운영(docker-compose.yml 단독)은 그대로 nginx 정적 호스팅으로 동작합니다.

Redis 는 기본적으로 뜨지 않습니다. 필요해지면 루트 .env.{env} 에 COMPOSE_PROFILES=redis 를 설정하세요.

프론트엔드 실행 방식

방식 명령 첫 서빙 용도
호스트 vite (개발 기본) cd frontend && npm run dev 1~2초 (최초 의존성 사전번들 10초 내외) 일상 개발
컨테이너 vite (선택) docker compose ... --profile frontend up -d 수 분 (Windows 바인드 마운트) 호스트에 Node 를 둘 수 없을 때
nginx 정적 (운영) docker compose --env-file .env.production up -d --build - 배포

컨테이너 방식을 상시 사용하려면 루트 .env.development 에 COMPOSE_PROFILES=frontend 를 넣습니다. (redis 와 함께 켤 때는 COMPOSE_PROFILES=frontend,redis) 호스트 vite 와 컨테이너 vite 는 같은 호스트 포트를 쓰므로 동시에 띄우지 마세요.

포트는 루트 .env.{env} 를 따릅니다. 호스트에서 띄우는 vite 도 FRONTEND_PORT 로 열리고, /api 프록시 대상은 BACKEND_PORT 에서 나옵니다 — vite.config.ts 가 루트 env 를 직접 읽습니다. 포트가 이미 사용 중이면 조용히 다른 포트로 옮기지 않고 실패합니다 (대개 다른 vite 가 이미 그 포트에 떠 있다는 뜻이라, 말없이 옮겨가면 브라우저가 엉뚱한 서버를 보게 됩니다).

전체 실행/배포 명령어는 .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 가 기동하지 않습니다.

docker network create proxy                     # 이미 있으면 생략
docker network connect proxy <NPM 컨테이너 이름>  # NPM 이 아직 proxy 네트워크에 없다면

NPM 은 <PROJECT_NAME>-frontend 같은 컨테이너 이름으로 접근하므로, 같은 서버에 여러 프로젝트를 올린다면 PROJECT_NAME 이 겹치지 않게 합니다.

2) 앱 기동

cp .env.production.example .env.production                 # placeholder 교체 — BACKEND_API_URL=/api 는 그대로 둔다
cp backend/.env.production.example backend/.env.production

# 업로드 디렉터리 소유자를 앱 사용자(node, uid 1000)로 — Docker 가 root 소유로 만들면 백엔드가 기동에 실패한다
mkdir -p data/uploads && sudo chown 1000:1000 data/uploads

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 프록시와 같은 규칙입니다. (Swagger 는 운영에서 기본으로 꺼져 있습니다. 켜려면 SWAGGER_ENABLED=true + NPM Access List 로 접근을 제한하세요)
  • Forward Hostname 에 경로를 붙이지 않습니다. 요청 경로가 그대로 넘어가야 백엔드의 /api prefix 와 맞습니다.
  • /api 로 시작하는 경로는 프론트 라우트로 쓰지 않습니다 (전부 백엔드로 갑니다).

4) 확인

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 대상이 아니므로 로컬에서 직접 실행합니다.

cd flutter && cp .env.example .env && flutter pub get && flutter run

flutter/.env 의 API_URL 을 백엔드 주소로 설정합니다. (Android 에뮬레이터는 http://10.0.2.2:3000/api, 운영은 웹과 같은 도메인의 https://app.your-domain.com/api)

환경변수

.env* 는 커밋하지 않습니다. 각 디렉터리의 *.example 파일을 복사해 사용하세요.

루트 .env.{env} 가 단일 진실 공급원(SSOT) 입니다. backend·frontend 는 docker-compose 가 루트 값을 주입하므로 자체 env 에 값을 중복 작성하지 않습니다. (자세한 위임 구조는 CLAUDE.md 의 "환경변수 구조" 참조)

파일 용도 값 작성 위치
.env.{env}.example 루트 — 모든 설정의 SSOT (compose 주입) ✅ 여기에 작성
backend/.env.{env}.example 백엔드 — 고유 시크릿(JWT 등)만 최소 (존재 필수)
frontend/.env.{env}.example 프론트 — 빌드 시 루트 BACKEND_API_URL(기본 /api) 주입 작성 불필요
flutter/.env.example 모바일 — API_URL (compose 비대상) ✅ 독립 관리

{env} = development | production

표준 응답 포맷

// 성공
{ "success": true, "data": { /* ... */ } }

// 실패 — 사용자에게 보이는 모든 실패는 에러 코드를 가진다
{ "success": false, "error": { "code": "VAL_001", "message": "...", "traceId": "3f1c0a2e-..." } }

// 목록 — data 안의 형태가 고정되어 있다
{ "success": true, "data": { "items": [], "pageInfo": { "hasNext": false, "nextCursor": null, "total": 0 } } }

traceId 는 요청마다 발급되는 추적 ID(x-request-id 헤더와 동일)로, 서버 로그에 같은 값이 남습니다. 사용자 문의가 들어오면 이 값으로 로그를 검색하면 됩니다.

/api/health 는 예외입니다. 모니터링 도구가 읽는 엔드포인트라 래퍼 없이 원본 형식을 반환합니다.

에러 코드는 한 곳에서 관리됩니다

backend/src/common/errors/error-codes.ts   ← 여기만 수정
        │  cd backend && npm run sync:error-codes
        ├─▶ frontend/src/shared/constants/error-codes.generated.ts   (자동 생성)
        └─▶ flutter/lib/core/api/error_lexicon.g.dart                (자동 생성)

생성 파일은 직접 수정하지 마세요. CI 가 동기화 여부를 검사합니다. 자세한 규칙은 .claude/rules/error-codes.md 참조.

로컬 개발 (Docker 없이)

DB 는 컨테이너로 띄우고 앱만 호스트에서 실행하는 방식이 가장 간단합니다.

# 1) DB 만 컨테이너로 기동 (127.0.0.1:5432 로 노출된다)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d db

# 2) backend — DB_HOST 기본값이 localhost 라 그대로 붙는다
cd backend && npm ci && npm run start:dev

# 3) frontend — vite dev 서버가 /api 를 localhost:3000 으로 프록시한다 (개발 기본 방식)
cd frontend && npm ci && npm run dev

# 4) flutter
cd flutter && flutter pub get && flutter run

프론트엔드의 API 요청은 /api 상대경로로 나가고 vite dev 프록시가 백엔드로 넘깁니다. 백엔드를 다른 주소에서 띄웠다면 VITE_DEV_API_PROXY 로 대상을 바꾸세요.

커밋 전 검사

cd backend  && npm run lint && npm run test && npm run build
cd frontend && npm run lint && npm run type-check && npm run test:unit
cd flutter  && flutter analyze && flutter test

lint 는 --max-warnings 0 이라 경고 하나만 있어도 실패합니다. 자동 수정은 npm run lint:fix. 같은 검사를 CI(.github/workflows/ci.yml)가 PR 마다 수행합니다.

⚠️ 의존성을 추가·제거할 때는 컨테이너 안에서 실행하세요. Windows 에서 npm install 하면 Linux 전용 패키지가 lock 에서 잘려 Docker 빌드가 깨집니다.

docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development exec backend npm install <패키지>

개발 규칙

영역 문서
공통/보안 CLAUDE.md · .claude/rules/
응답·에러 response-format.md · error-codes.md
API 설계 api-design.md
로깅 logging.md
체크리스트 checklist.md
NestJS .claude/skills/nestjs/SKILL.md
Vue 3 .claude/skills/vue3/SKILL.md
Flutter .claude/skills/flutter/SKILL.md
Docker .claude/skills/docker/SKILL.md
역할 분담 .claude/agents/README.md

디렉터리 구조

.
├── backend/    # NestJS API (Controller / Service / DTO / 공통 필터·인터셉터)
├── frontend/   # Vue 3 (pages / components / composables / stores / router)
├── flutter/    # Flutter (core / features / shared)
├── docker-compose.yml         # 베이스 정의
├── docker-compose.dev.yml     # 개발 오버라이드 (핫리로드, 포트 노출)
├── CLAUDE.md   # 프로젝트 개요 + 공통 규칙 @import
├── .github/
│   ├── workflows/ci.yml       # lint·test·build + 에러 코드 동기화 검사
│   └── pull_request_template.md
├── ci/                        # CI 러너 (Gitea Actions) — 앱 스택과 분리
└── .claude/
    ├── settings.json   # Claude Code 권한·환경 설정
    ├── rules/          # 공통 규칙 (코딩·응답 포맷·에러 코드·API 설계·로깅·보안·환경변수·테스트)
    ├── skills/         # 도메인별 개발 규칙 (nestjs·vue3·flutter·docker)
    └── agents/         # 역할별 규칙 (개발 3종·테스터·리뷰어·인프라·사용자/관리자 관점)