다크 모드
Appendix A. 사내 실무 환경설정 및 Docker/Nginx 배포 가이드
"로컬에서만 잘 돌아가는 코드는 가치가 반감된다. 개발, 검증, 운영 환경을 안전하게 분기하고, Node 22 LTS 멀티스테이지 Docker 빌드와 Nginx 리버스 프록시로 프로덕션에 무중단 배포하는 엔터프라이즈 실무 표준을 정립한다."
📋 이 부록에서 처음 만나는 개념 — 모르는 문법이 나오면 0장 준비운동에서 찾아볼 수 있습니다.
| 개념 | 한 줄 요약 | 설명 위치 |
|---|---|---|
environment.ts & fileReplacements | 빌드 시점에 로컬·검증·운영 환경설정 파일을 자동으로 바꿔 끼우는 Angular 표준 | A.1 |
APP_CONFIG InjectionToken | 환경설정 객체를 컴포넌트와 레포지토리에 DI로 안전하게 주입하는 패턴 | A.1 [코드 A-2] |
| Docker 멀티스테이지 빌드 | Node 22 LTS 빌드용 이미지와 25MB 초경량 Nginx 실행용 이미지를 분리 | A.2 [코드 A-4] |
Nginx try_files, 캐시 헤더, proxy_pass | SPA 히스토리 라우팅 404 방지, 배포 직후 청크 불일치 방어, API 리버스 프록시 | A.3 [코드 A-5] |
host.docker.internal | 컨테이너 안에서 호스트 PC의 Spring Boot(8080)를 가리키는 도메인 | A.3, A.4 |
A.1 환경설정 분기 전략 (environments & fileReplacements)
사내 시스템은 보통 로컬(Local), 개발/검증(Dev/Staging), 운영(Production) 환경으로 나뉩니다. 코드 안에 http://localhost:8080을 하드코딩해서는 안 됩니다. Angular CLI는 angular.json의 fileReplacements 설정을 통해 빌드 타깃에 맞는 환경설정 파일을 컴파일 시점에 자동으로 교체해 줍니다.
[코드 A-1] 로컬 및 운영 환경설정 파일 (src/environments/)
typescript
// 1. 로컬 개발 환경: frontend/src/environments/environment.ts
export interface AppEnvironment {
production: boolean;
envName: 'local' | 'staging' | 'production';
apiBaseUrl: string;
}
export const environment: AppEnvironment = {
production: false,
envName: 'local',
// 로컬에서는 proxy.conf.json(/api -> localhost:8080)을 타도록 빈 문자열 지정
apiBaseUrl: ''
};typescript
// 2. 사내 운영 환경: frontend/src/environments/environment.prod.ts
import { AppEnvironment } from './environment';
export const environment: AppEnvironment = {
production: true,
envName: 'production',
// 운영에서도 Nginx 동일 출처(Same-Origin) 리버스 프록시(/api/)를 타도록 빈 문자열 유지
apiBaseUrl: ''
};[코드 A-2] angular.json의 fileReplacements와 InjectionToken 연결
json
// frontend/angular.json (architect.build.configurations.production 발췌)
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false
}
}typescript
// frontend/src/app/core/app-config.token.ts
import { InjectionToken } from '@angular/core';
import { AppEnvironment, environment } from '../../environments/environment';
// 🌟 전역에서 inject(APP_CONFIG)로 환경설정을 꺼내 쓸 수 있는 토큰 (테스트 시 덮어쓰기 용이!)
export const APP_CONFIG = new InjectionToken<AppEnvironment>('APP_CONFIG', {
providedIn: 'root',
factory: () => environment
});CAUTION
environment.prod.ts의 값도 비밀(Secret)이 아닙니다!
Angular가 빌드할 때 environment.prod.ts의 내용은 최종 JavaScript 번들(main-[hash].js) 안에 평문 문자열 그대로 포함됩니다. 브라우저 개발자 도구로 누구나 열람할 수 있으므로 DB 비밀번호, AWS Secret Key, 내부 관리자 마스터 토큰은 절대 프론트엔드 환경변수에 넣지 마십시오. 민감 정보는 오직 백엔드(Spring Boot 서버 환경변수나 Vault)에만 두어야 합니다.
TIP
왜 운영 환경에서도 동일 출처(Same-Origin) /api 리버스 프록시를 권장할까요?apiBaseUrl: 'https://api.company.com'처럼 프론트엔드 도메인과 다른 주소로 호출하면 매번 CORS Preflight(OPTIONS 요청) 네트워크 왕복 비용이 발생하고, HttpOnly 보안 쿠키 전송 설정(SameSite, withCredentials)이 까다로워집니다. 아래 nginx.conf처럼 Nginx가 /api/ 요청을 백엔드 서버로 프록시(proxy_pass)하도록 구성하면 CORS 문제를 아예 원천 소멸시킬 수 있습니다!
A.2 프로덕션 배포를 위한 .dockerignore와 멀티스테이지 Dockerfile
Angular 22 애플리케이션은 빌드(ng build --configuration production)하고 나면 순수한 정적 파일(dist/frontend/browser/ 폴더 안의 HTML, CSS, JS)만 남습니다.
따라서 운영 컨테이너에 무거운 Node.js 런타임이나 node_modules(수백 MB)를 넣을 필요가 전혀 없습니다. **1단계 빌드 스테이지(node:22-alpine)**에서 정적 번들만 구워내고, **2단계 서빙 스테이지(nginx:alpine)**로 결과물만 복사하면 약 25~30MB의 초경량 보안 컨테이너가 완성됩니다.
[코드 A-3] frontend/.dockerignore
호스트 OS(Windows/macOS)의 무거운 node_modules나 빌드 캐시가 Docker 빌드 컨텍스트로 복사되지 않도록 반드시 .dockerignore를 먼저 작성합니다:
txt
node_modules
dist
.angular
.git
.vscode
coverage
*.log[코드 A-4] 멀티스테이지 빌드 (frontend/Dockerfile)
dockerfile
# =========================================================================
# 1단계: 빌드 스테이지 (Angular 22 필수 사양인 Node.js 22 LTS 알파인 이미지)
# =========================================================================
FROM node:22-alpine AS builder
WORKDIR /app
# Docker 레이어 캐시 극대화: 소스 코드보다 package*.json을 먼저 복사해 의존성 설치
COPY package*.json ./
RUN npm ci
# 전체 프론트엔드 소스 복사 및 프로덕션 AOT 빌드 실행
COPY . .
RUN npm run build -- --configuration production
# =========================================================================
# 2단계: 서빙 스테이지 (초경량 Nginx 알파인 이미지)
# =========================================================================
FROM nginx:alpine
# 기본 Nginx 설정 제거 후 엔터프라이즈 SPA 전용 설정 주입
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 🌟 Angular 17~22+ Application Builder의 출력 경로는 dist/<프로젝트명>/browser 입니다!
COPY --from=builder /app/dist/frontend/browser /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]🆕 처음 보는 문법: Angular Application Builder 출력 경로 (dist/frontend/browser)
Angular의 최신 esbuild/Vite 기반 application 빌더는 SSR/프리렌더링 호환을 위해 정적 브라우저 에셋을 dist/<project-name>/browser 폴더 아래에 생성합니다(index.html이 이 안에 있습니다). 만약 예전 버전처럼 /app/dist/frontend만 복사하면 Nginx 루트 바로 아래에 browser/ 하위 폴더가 생겨 403 Forbidden이나 404 Not Found가 발생하므로 경로 끝의 /browser를 꼭 확인하십시오!
A.3 SPA 라우팅과 배포 직후 청크 에러 방지를 위한 nginx.conf
Angular Router(provideRouter(routes))로 만든 SPA를 Nginx로 서빙할 때 반드시 챙겨야 할 3대 핵심 설정이 있습니다:
- SPA Fallback (
try_files $uri $uri/ /index.html): 사용자가/write나/admin경로에서 브라우저 새로고침(F5)을 눌렀을 때, Nginx가 물리 파일을 찾다404 Not Found를 내지 않고/index.html을 내려주어야 Angular Router가 화면을 이어받습니다. index.html캐시 절대 금지 (no-cache, no-store): 브라우저가 옛날index.html을 캐싱하고 있으면, 신규 배포 직후 해시 이름이 바뀐 지연 로딩 청크(chunk-XYZ.js)를 요청하다가ChunkLoadError/Failed to fetch dynamically imported module에러가 터집니다! 반면 해시가 붙은.js/.css파일은1y immutable로 영구 캐싱합니다./api/백엔드 리버스 프록시:/api/요청을 Spring Boot 서버(8080)로 토스합니다.
[코드 A-5] 엔터프라이즈 frontend/nginx.conf
nginx
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip 텍스트 압축 전송 (초기 JS/CSS 번들 크기 70% 이상 절감)
gzip on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
# 1. index.html은 절대 캐싱하지 않음 (신규 배포 즉시 새 해시 청크를 바라보도록 강제!)
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 2. Angular CLI가 파일명에 해시(outputHashing: all)를 붙인 정적 에셋은 1년간 영구 캐싱
location ~* \.(?:js|css|woff2?|ttf|eot|svg|png|jpe?g|gif|ico)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
# 3. SPA 히스토리 라우팅 폴백: 정적 파일이 없으면 index.html로 위임
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
# 4. Spring Boot 3 백엔드 API 리버스 프록시 (동일 출처 통신으로 CORS 원천 해결)
# - 로컬 Docker Desktop 실습 시: host.docker.internal:8080 사용
# - Docker Compose / Kubernetes 운영 시: http://backend-service:8080/api/ 로 변경
location /api/ {
proxy_pass http://host.docker.internal:8080/api/;
proxy_http_version 1.1;
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_read_timeout 30s;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}A.4 컨테이너 빌드, 기동 및 검증 명령어
사전 조건: 호스트 PC의 8080 포트에서 Spring Boot 백엔드가 실행 중이어야 합니다(cd backend && ./mvnw spring-boot:run).
bash
# 1. frontend 폴더로 이동 후 프로덕션 Docker 이미지 빌드
cd frontend
docker build -t angular-board-fe:1.0.0 .
# 2-A. 컨테이너 기동 (Windows / macOS Docker Desktop)
docker run -d -p 8081:80 --name board-fe angular-board-fe:1.0.0
# 2-B. 컨테이너 기동 (Linux Docker Engine: host.docker.internal 게이트웨이 명시 필수!)
docker run -d -p 8081:80 \
--add-host=host.docker.internal:host-gateway \
--name board-fe angular-board-fe:1.0.0
# 3. 컨테이너 정상 기동 및 Nginx 로그 확인
docker ps
docker logs board-fe배포 후 3단계 스모크 테스트 (Smoke Test)
- 초기 화면 및 API 프록시 확인: 브라우저에서
http://localhost:8081에 접속해 게시글 목록이 정상적으로 로드되는지 확인합니다. - SPA 새로고침(F5) 확인:
http://localhost:8081/write로 이동(로그인 전이면/login?returnUrl=%2Fwrite로 리다이렉트됨)한 상태에서 브라우저 **새로고침(F5)**을 눌렀을 때 Nginx 404 에러 없이 화면이 그대로 뜨는지 확인합니다. - 캐시 헤더 확인: 개발자 도구(F12) → Network 탭에서
index.html의 응답 헤더는Cache-Control: no-cache, no-store, must-revalidate이고,main-XXXX.js의 응답 헤더는Cache-Control: public, max-age=31536000, immutable인지 확인합니다.