Search

React + Spring Boot ν’€μŠ€νƒ 배포

React + Spring Boot ν’€μŠ€νƒ 배포

κ°œμš”

ν’€μŠ€νƒ λ°°ν¬λž€ ν”„λ‘ νŠΈμ—”λ“œ(React)와 λ°±μ—”λ“œ(Spring Boot)λ₯Ό ν•˜λ‚˜μ˜ Nginxμ—μ„œ λ™μ‹œμ— μ„œλΉ™ν•˜λŠ” ꡬ성이닀. 정적 νŒŒμΌμ€ Nginxκ°€ 직접 μ²˜λ¦¬ν•˜κ³ , API μš”μ²­μ€ Spring Boot둜 ν”„λ‘μ‹œν•œλ‹€.
μ§€κΈˆκΉŒμ§€ React 단독 배포, Spring Boot 단독 배포λ₯Ό λ°°μ› μ£ ? 이제 이 λ‘˜μ„ ν•˜λ‚˜λ‘œ ν•©μ³μ„œ λ°°ν¬ν•˜λŠ” 법을 배울 κ±°μ˜ˆμš”!
μ‹€λ¬΄μ—μ„œ κ°€μž₯ ν”ν•œ κ΅¬μ„±μ΄μ—μš”. μ‚¬μš©μžκ°€ example.com에 μ ‘μ†ν•˜λ©΄ React 화면이 보이고, Reactμ—μ„œ APIλ₯Ό ν˜ΈμΆœν•˜λ©΄ Spring Bootκ°€ μ²˜λ¦¬ν•΄μ„œ κ²°κ³Όλ₯Ό λŒλ €μ£ΌλŠ” κ±°μ£ . 이 전체λ₯Ό Nginx ν•˜λ‚˜κ°€ μ•žλ‹¨μ—μ„œ κ΅ν†΅μ •λ¦¬ν•΄μ€˜μš”!

ν’€μŠ€νƒ 배포 μ•„ν‚€ν…μ²˜

graph TD
    A[πŸ‘€ μ‚¬μš©μž<br/>λΈŒλΌμš°μ €] -->|"<https://example.com>"| B[πŸ–₯️ Nginx<br/>포트 443]

    B -->|"/ (정적 μš”μ²­)<br/>HTML, CSS, JS"| C["πŸ“ /var/www/frontend/<br/>React λΉŒλ“œ κ²°κ³Όλ¬Ό"]
    B -->|"/api/ (동적 μš”μ²­)<br/>REST API"| D["πŸƒ Spring Boot<br/>localhost:8080"]

    D --> E["πŸ—„οΈ Database<br/>MySQL/PostgreSQL"]

    style B fill:#FFD700
    style C fill:#90EE90
    style D fill:#87CEEB
Mermaid
볡사

μš”μ²­ 흐름 상세

sequenceDiagram
    participant μ‚¬μš©μž as πŸ‘€ μ‚¬μš©μž
    participant Nginx as πŸ–₯️ Nginx
    participant React as πŸ“ React (정적 파일)
    participant Spring as πŸƒ Spring Boot

    μ‚¬μš©μž->>Nginx: GET / (νŽ˜μ΄μ§€ 접속)
    Nginx->>React: index.html λ°˜ν™˜
    React-->>Nginx: HTML + JS + CSS
    Nginx-->>μ‚¬μš©μž: νŽ˜μ΄μ§€ λ Œλ”λ§

    μ‚¬μš©μž->>Nginx: GET /api/users (API 호좜)
    Nginx->>Spring: λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ 전달
    Spring-->>Nginx: JSON 응닡
    Nginx-->>μ‚¬μš©μž: μ‚¬μš©μž λͺ©λ‘ 데이터

    μ‚¬μš©μž->>Nginx: GET /static/logo.png (이미지)
    Nginx->>React: 정적 파일 직접 λ°˜ν™˜
    React-->>Nginx: 이미지 파일
    Nginx-->>μ‚¬μš©μž: 이미지 ν‘œμ‹œ
Mermaid
볡사

URL κ²½λ‘œλ³„ 처리 λ§€ν•‘

URL νŒ¨ν„΄
처리 방식
λŒ€μƒ
μ˜ˆμ‹œ
/
정적 μ„œλΉ™
React index.html
메인 νŽ˜μ΄μ§€
/about, /users
SPA λΌμš°νŒ…
React index.html β†’ React Router
ν΄λΌμ΄μ–ΈνŠΈ λΌμš°νŒ…
/static/
정적 μ„œλΉ™ + 캐싱
React λΉŒλ“œ 에셋
JS, CSS, 이미지
/api/
λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ
Spring Boot
REST API
/actuator/health
λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ
Spring Boot
ν—¬μŠ€μ²΄ν¬

Nginx μ„€μ • (전체)

sudo nano /etc/nginx/sites-available/fullstack-app
Bash
볡사
# λ°±μ—”λ“œ μ—…μŠ€νŠΈλ¦Ό upstream spring_backend { server localhost:8080; } server { listen 443 ssl http2; server_name example.com www.example.com; # SSL μΈμ¦μ„œ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # ============================================ # πŸ“ ν”„λ‘ νŠΈμ—”λ“œ (React) - 정적 파일 μ„œλΉ™ # ============================================ root /var/www/frontend; index index.html; location / { try_files $uri $uri/ /index.html; # SPA λΌμš°νŒ… λŒ€μ‘ } # React 정적 에셋 캐싱 (ν•΄μ‹œ ν¬ν•¨λœ 파일) location /static/ { expires 1y; add_header Cache-Control "public, immutable"; } # ============================================ # πŸƒ λ°±μ—”λ“œ (Spring Boot) - λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ # ============================================ location /api/ { proxy_pass http://spring_backend; 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_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # ============================================ # πŸ”§ 기타 μ„€μ • # ============================================ # 파일 μ—…λ‘œλ“œ 크기 μ œν•œ client_max_body_size 10M; # gzip μ••μΆ• gzip on; gzip_vary on; gzip_min_length 256; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; # μ—λŸ¬ νŽ˜μ΄μ§€ error_page 404 /index.html; error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } # HTTP β†’ HTTPS λ¦¬λ‹€μ΄λ ‰νŠΈ server { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; }
Plain Text
볡사

μ„€μ • 핡심 포인트

graph TD
    A["Nginx μš”μ²­ λΆ„λ₯˜"] --> B{"URL μ‹œμž‘μ΄<br/>/api/ 인가?"}
    B -->|Yes| C["location /api/<br/>β†’ Spring Boot ν”„λ‘μ‹œ"]
    B -->|No| D{"μ‹€μ œ 파일이<br/>μ‘΄μž¬ν•˜λŠ”κ°€?"}
    D -->|Yes| E["ν•΄λ‹Ή 파일 λ°˜ν™˜<br/>(CSS, JS, 이미지 λ“±)"]
    D -->|No| F["index.html λ°˜ν™˜<br/>β†’ React Router 처리"]

    style C fill:#87CEEB
    style E fill:#90EE90
    style F fill:#FFD700
Mermaid
볡사
핡심 μ„€μ •
μ„€λͺ…
μ™œ ν•„μš”?
try_files $uri $uri/ /index.html
파일 μ—†μœΌλ©΄ index.html λ°˜ν™˜
React Routerκ°€ ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ λΌμš°νŒ…
location /api/ + proxy_pass
API 경둜만 Spring Boot둜 전달
ν”„λ‘ νŠΈ/λ°±μ—”λ“œ 뢄리
proxy_set_header
원본 μš”μ²­ 정보 전달
Spring Bootκ°€ μ‹€μ œ μ‚¬μš©μž 정보 인식
expires 1y (static/)
정적 에셋 μž₯κΈ° 캐싱
λΉŒλ“œλ§ˆλ‹€ ν•΄μ‹œκ°€ λ³€ν•˜λ―€λ‘œ μ•ˆμ „
gzip on
응닡 μ••μΆ•
전솑 속도 ν–₯상 (30~70% 크기 κ°μ†Œ)

React API 호좜 μ„€μ •

Reactμ—μ„œ APIλ₯Ό ν˜ΈμΆœν•  λ•Œ 같은 λ„λ©”μΈμ΄λ―€λ‘œ 별도 URL 섀정이 ν•„μš” μ—†μ–΄μš”!
// Reactμ—μ„œ API 호좜 (같은 λ„λ©”μΈμ΄λ―€λ‘œ /api/만 μ‚¬μš©) const response = await fetch('/api/users'); const users = await response.json(); // ν™˜κ²½λ³„ 섀정이 ν•„μš”ν•˜λ©΄ ν™˜κ²½λ³€μˆ˜ μ‚¬μš© const API_BASE = process.env.REACT_APP_API_URL || ''; const response = await fetch(`${API_BASE}/api/users`);
JavaScript
볡사
ν™˜κ²½
API URL
μ„€λͺ…
둜컬 개발
http://localhost:8080/api
CRA proxy λ˜λŠ” 직접 μ§€μ •
ν”„λ‘œλ•μ…˜
/api (μƒλŒ€ 경둜)
Nginxκ°€ 같은 λ„λ©”μΈμ—μ„œ 처리

둜컬 개발 μ‹œ ν”„λ‘μ‹œ μ„€μ •

package.json:
{ "proxy": "<http://localhost:8080>" }
JSON
볡사
μ΄λ ‡κ²Œ ν•˜λ©΄ λ‘œμ»¬μ—μ„œλ„ /api/users둜 μš”μ²­ν•˜λ©΄ μžλ™μœΌλ‘œ localhost:8080/api/users둜 μ „λ‹¬λΌμš”!

μ„œλ²„ 디렉토리 ꡬ쑰

/var/www/frontend/ ← React λΉŒλ“œ κ²°κ³Όλ¬Ό β”œβ”€β”€ index.html β”œβ”€β”€ static/ β”‚ β”œβ”€β”€ js/ β”‚ β”œβ”€β”€ css/ β”‚ └── media/ β”œβ”€β”€ favicon.ico └── manifest.json /home/ubuntu/app/ ← Spring Boot β”œβ”€β”€ myapp.jar β”œβ”€β”€ application-prod.yml (선택) └── logs/ (선택)
Plain Text
볡사

배포 μžλ™ν™” 슀크립트

#!/bin/bash # deploy.sh β€” ν’€μŠ€νƒ 배포 슀크립트 set -e # μ—λŸ¬ λ°œμƒ μ‹œ 쀑단 echo "πŸ“¦ ν”„λ‘ νŠΈμ—”λ“œ λΉŒλ“œ..." cd /home/ubuntu/my-react-app git pull origin main npm install npm run build echo "πŸ“ ν”„λ‘ νŠΈμ—”λ“œ 배포..." sudo rm -rf /var/www/frontend/* sudo cp -r build/* /var/www/frontend/ sudo chown -R www-data:www-data /var/www/frontend echo "πŸƒ λ°±μ—”λ“œ λΉŒλ“œ 및 배포..." cd /home/ubuntu/my-spring-app git pull origin main ./gradlew clean build -x test echo "πŸ”„ λ°±μ—”λ“œ μž¬μ‹œμž‘..." sudo systemctl restart myapp echo "⏳ Spring Boot μ‹œμž‘ λŒ€κΈ°..." sleep 10 echo "πŸ§ͺ ν—¬μŠ€ 체크..." curl -f <http://localhost:8080/actuator/health> || echo "⚠️ ν—¬μŠ€μ²΄ν¬ μ‹€νŒ¨!" echo "βœ… 배포 μ™„λ£Œ!"
Bash
볡사

자주 λ°œμƒν•˜λŠ” 문제

문제
원인
ν•΄κ²°
API 호좜 μ‹œ 404
location /api/ μ„€μ • λˆ„λ½
/api/ location 블둝 μΆ”κ°€
CORS μ—λŸ¬
ν”„λ‘ νŠΈ/λ°±μ—”λ“œ ν¬νŠΈκ°€ 닀름 (κ°œλ°œν™˜κ²½)
proxy μ„€μ • λ˜λŠ” CORS ν—ˆμš©
React λΌμš°νŒ… ν›„ 404
try_files λ―Έμ„€μ •
try_files $uri $uri/ /index.html
API 느림 (502/504)
Spring Boot λ―Έμ‹œμž‘ λ˜λŠ” κ³ΌλΆ€ν•˜
μ„œλΉ„μŠ€ μƒνƒœ 확인, νƒ€μž„μ•„μ›ƒ μ‘°μ •
λΉŒλ“œ 파일 반영 μ•ˆ 됨
μΊμ‹œ 문제
λΈŒλΌμš°μ € μΊμ‹œ μ‚­μ œ, CDN λ¬΄νš¨ν™”

μ‹€μŠ΅ β€” k-rules ν”„λ‘œμ νŠΈ ν’€μŠ€νƒ 배포

μ‹€μŠ΅ ν™˜κ²½

ν•­λͺ©
κ°’
ν”„λ‘œμ νŠΈ 루트
C:\\DEV\\k-rules\\
ν”„λ‘ νŠΈμ—”λ“œ
C:\\DEV\\k-rules\\frontend (Vite + React)
λ°±μ—”λ“œ
C:\\DEV\\k-rules\\backend (Spring Boot + Gradle)
μ„œλ²„
alohaserver4.cafe24.com
도메인
κ΅­λ£°.com (xn--3e0b91t.com)
ν”„λ‘ νŠΈ 배포 경둜
/var/www/krules/frontend/
λ°±μ—”λ“œ 배포 경둜
/var/www/krules/backend/
Spring Boot 포트
8080 (λ‚΄λΆ€ 톡신 μ „μš©)
SSH 접속
Host alias alohaserver4
λͺ©ν‘œ: 도메인 ν•˜λ‚˜(κ΅­λ£°.com)둜 ν”„λ‘ νŠΈ+λ°±μ—”λ“œλ₯Ό λͺ¨λ‘ μ„œλΉ™. / β†’ React, /api/ β†’ Spring Boot. CORS 이슈 없이 OAuth μ†Œμ…œ λ‘œκ·ΈμΈκΉŒμ§€ μ™„λ²½ν•˜κ²Œ λ™μž‘ν•˜λ„λ‘ ꡬ성!

ν”„λ‘œμ νŠΈ ꡬ쑰

πŸ“Β C:\\DEV\\k-rules\\ β”œβ”€β”€ πŸ“ frontend\\ ← React Vite ν”„λ‘œμ νŠΈ β”‚ β”œβ”€β”€ πŸ“Β src\\ β”‚ β”œβ”€β”€ πŸ“Β dist\\ ← λΉŒλ“œ κ²°κ³Ό (npm run build) β”‚ β”œβ”€β”€ πŸ“„Β .env.local β”‚ β”œβ”€β”€ πŸ“„Β .env.production β”‚ └── πŸ“„Β deploy.bat ← ν”„λ‘ νŠΈ 배포 μžλ™ν™” β”‚ β”œβ”€β”€ πŸ“ backend\\ ← Spring Boot ν”„λ‘œμ νŠΈ β”‚ β”œβ”€β”€ πŸ“ src\\ β”‚ β”œβ”€β”€ πŸ’» build\\libs\\APP.war ← λΉŒλ“œ κ²°κ³Ό (bootWar) β”‚ β”œβ”€β”€ πŸ“œΒ start.sh / stop.sh / restart.sh β”‚ └── πŸ“„Β deploy.bat ← λ°±μ—”λ“œ 배포 μžλ™ν™” β”‚ └── πŸ“„Β deploy-all.bat ← 🌟 ν’€μŠ€νƒ 톡합 배포
Plain Text
볡사

μ„œλ²„ ꡬ쑰

πŸ“Β /var/www/krules/ β”œβ”€β”€ πŸ“Β frontend/ ← React 정적 파일 β”‚ β”œβ”€β”€ πŸ“„Β index.html β”‚ └── πŸ“Β assets/ β”‚ β”œβ”€β”€ πŸ“Β backend/ ← Spring Boot WAR β”‚ β”œβ”€β”€ πŸ’»Β APP.war β”‚ β”œβ”€β”€ πŸ“œΒ start.sh β”‚ β”œβ”€β”€ πŸ“œΒ stop.sh β”‚ └── πŸ“œΒ restart.sh β”‚ └── πŸ“Β log/ └── πŸ“°Β appwar_*.log ← Spring Boot μ‹€ν–‰ 둜그
Plain Text
볡사

사전 μ€€λΉ„ (졜초 1회)

# μ„œλ²„ 접속 ssh alohaserver4 # 디렉토리 생성 mkdir -p /var/www/krules/frontend mkdir -p /var/www/krules/backend mkdir -p /var/www/krules/log
Bash
볡사

1단계: ν”„λ‘ νŠΈμ—”λ“œ .env μ„€μ •

C:\\DEV\\k-rules\\frontend\\.env.local (둜컬 개발용, .gitignore μΆ”κ°€):
# ViteλŠ” VITE_ 접두사 ν•„μˆ˜ VITE_API_URL=http://localhost:8080/api # VITE_API_URL=http://192.168.30.19:8080/api # 사내망 ν…ŒμŠ€νŠΈ
Bash
볡사
C:\\DEV\\k-rules\\frontend\\.env.production (배포용):
# ν’€μŠ€νƒ = 같은 도메인 β†’ μƒλŒ€ 경둜 μ‚¬μš© ꢌμž₯ VITE_API_URL=/api # λ˜λŠ” λͺ…μ‹œμ μœΌλ‘œ # VITE_API_URL=https://xn--3e0b91t.com/api
Bash
볡사
React μ½”λ“œμ—μ„œ:
// 둜컬: <http://localhost:8080/api/products> // 배포: /api/products (같은 도메인) const res = await fetch(`${import.meta.env.VITE_API_URL}/products`);
JavaScript
볡사
μƒλŒ€ 경둜(/api)의 μž₯점: 도메인이 λ°”λ€Œκ±°λ‚˜ HTTPβ†’HTTPS μ „ν™˜ν•΄λ„ μ½”λ“œ μˆ˜μ • λΆˆν•„μš”! Nginxκ°€ ν˜„μž¬ λ„λ©”μΈμ—μ„œ /api/λ₯Ό Spring Boot둜 μ „λ‹¬ν•΄μ£Όλ‹ˆκΉŒμš”.

2단계: Spring Boot application.yml μ„€μ •

C:\\DEV\\k-rules\\backend\\src\\main\\resources\\application.yml:
server: port: 8080 forward-headers-strategy: framework # ⭐ Nginx ν”„λ‘μ‹œ 헀더 인식 ν•„μˆ˜! spring: profiles: active: prod # μ„Έμ…˜ μΏ ν‚€ μ„€μ • (OAuth μ„Έμ…˜ μœ μ§€) session: cookie: secure: true # HTTPSμ—μ„œλ§Œ 전솑 http-only: true # JSμ—μ„œ μ ‘κ·Ό λΆˆκ°€ (XSS λ°©μ§€) same-site: lax # OAuth λ¦¬λ‹€μ΄λ ‰νŠΈ ν—ˆμš© # Actuator (ν—¬μŠ€μ²΄ν¬) management: endpoints: web: exposure: include: health
YAML
볡사
forward-headers-strategy: frameworkλ₯Ό κΌ­ μ„€μ •ν•˜μ„Έμš”! Spring Bootκ°€ X-Forwarded-Proto: https 헀더λ₯Ό 인식해야 OAuth λ¦¬λ‹€μ΄λ ‰νŠΈ URL을 https://둜 μ˜¬λ°”λ₯΄κ²Œ μƒμ„±ν•΄μš”.
이게 μ—†μœΌλ©΄ http://둜 λ¦¬λ‹€μ΄λ ‰νŠΈλ˜λŠ” OAuth κ΄€λ ¨ μ—λŸ¬κ°€ λ°œμƒ!

3단계: Nginx ν’€μŠ€νƒ 톡합 μ„€μ • (μ„œλ²„μ—μ„œ)

ssh alohaserver4 nano /etc/nginx/conf.d/krules.conf
Bash
볡사
# Spring Boot μ—…μŠ€νŠΈλ¦Ό upstream krules_backend { server localhost:8080; keepalive 32; } server { listen 80; server_name xn--3e0b91t.com www.xn--3e0b91t.com; # ════════════════════════════════════════ # πŸ“ ν”„λ‘ νŠΈμ—”λ“œ (React Vite) # ════════════════════════════════════════ root /var/www/krules/frontend; index index.html; # Vite 에셋 캐싱 (ν•΄μ‹œ 파일λͺ…이라 μ•ˆμ „ν•˜κ²Œ μž₯κΈ° μΊμ‹œ) location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } # ════════════════════════════════════════ # πŸƒ λ°±μ—”λ“œ API (Spring Boot) # ════════════════════════════════════════ location /api/ { proxy_pass http://krules_backend; 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_set_header X-Forwarded-Host $host; proxy_http_version 1.1; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; client_max_body_size 20M; } # ════════════════════════════════════════ # πŸ” OAuth μ†Œμ…œ 둜그인 경둜 (μ€‘μš”!) # ════════════════════════════════════════ # Spring Security OAuth2 κΈ°λ³Έ μ—”λ“œν¬μΈνŠΈλ₯Ό λ°±μ—”λ“œλ‘œ 전달 location /oauth2/ { proxy_pass http://krules_backend; 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_set_header X-Forwarded-Host $host; # πŸͺ OAuth μ„Έμ…˜ μΏ ν‚€κ°€ λ¦¬λ‹€μ΄λ ‰νŠΈ 후에도 μœ μ§€λ˜λ„λ‘ proxy_cookie_path / "/; Secure; SameSite=Lax"; } location /login/oauth2/ { proxy_pass http://krules_backend; 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_set_header X-Forwarded-Host $host; proxy_cookie_path / "/; Secure; SameSite=Lax"; } # ════════════════════════════════════════ # 🌐 React SPA λΌμš°νŒ… (맨 λ§ˆμ§€λ§‰μ—!) # ════════════════════════════════════════ location / { try_files $uri $uri/ /index.html; } # ════════════════════════════════════════ # 곡톡 μ„€μ • # ════════════════════════════════════════ gzip on; gzip_vary on; gzip_min_length 256; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; # 둜그 access_log /var/log/nginx/krules_access.log; error_log /var/log/nginx/krules_error.log; }
Plain Text
볡사
nginx -t && systemctl reload nginx
Bash
볡사

4단계: ν”„λ‘ νŠΈμ—”λ“œ 배포 (C:\\DEV\\k-rules\\frontend\\deploy.bat)

@echo off chcp 65001 > nul echo. echo ============================================= echo [Frontend] React Vite Build ^& Deploy echo ============================================= set PROJECT_DIR=%~dp0 set REMOTE_USER=root set REMOTE_HOST=alohaserver4.cafe24.com set REMOTE_PATH=/var/www/krules/frontend/ set REMOTE_PASS=❓❓❓❓❓ cd /d %PROJECT_DIR% echo [1/2] npm run build... call npm run build if errorlevel 1 ( pause & exit /b 1 ) echo [2/2] dist/ μ—…λ‘œλ“œ... pscp -pw %REMOTE_PASS% -r dist\\* %REMOTE_USER%@%REMOTE_HOST%:%REMOTE_PATH% if errorlevel 1 ( pause & exit /b 1 ) echo. echo ν”„λ‘ νŠΈμ—”λ“œ 배포 μ™„λ£Œ! echo <https://xn--3e0b91t.com> pause
Shell
볡사

5단계: λ°±μ—”λ“œ 배포 (C:\\DEV\\k-rules\\backend\\deploy.bat)

@echo off chcp 65001 > nul echo. echo ============================================= echo [Backend] Spring Boot bootWar ^& Deploy echo ============================================= set PROJECT_DIR=%~dp0 set REMOTE_USER=root set REMOTE_HOST=alohaserver4.cafe24.com set REMOTE_PATH=/var/www/krules/backend set REMOTE_PASS=❓❓❓❓❓ cd /d %PROJECT_DIR% echo [1/3] bootWar λΉŒλ“œ... call gradlew.bat clean bootWar if errorlevel 1 ( pause & exit /b 1 ) echo [2/3] APP.war + sh μ—…λ‘œλ“œ... pscp -pw %REMOTE_PASS% build\\libs\\APP.war %REMOTE_USER%@%REMOTE_HOST%:%REMOTE_PATH%/APP.war pscp -pw %REMOTE_PASS% start.sh stop.sh restart.sh %REMOTE_USER%@%REMOTE_HOST%:%REMOTE_PATH%/ if errorlevel 1 ( pause & exit /b 1 ) echo [3/3] μ„œλ²„μ—μ„œ restart.sh μ‹€ν–‰... plink -pw %REMOTE_PASS% %REMOTE_USER%@%REMOTE_HOST% "chmod +x %REMOTE_PATH%/*.sh && cd %REMOTE_PATH% && bash restart.sh" if errorlevel 1 ( pause & exit /b 1 ) echo. echo λ°±μ—”λ“œ 배포 μ™„λ£Œ! pause
Shell
볡사

6단계: ν’€μŠ€νƒ 톡합 배포 (C:\\DEV\\k-rules\\deploy-all.bat)

ν”„λ‘ νŠΈ + λ°±μ—”λ“œ ν•œ λ²ˆμ—! ν”„λ‘œμ νŠΈ λ£¨νŠΈμ— μ €μž₯ν•˜μ„Έμš”.
@echo off chcp 65001 > nul echo. echo ===================================================== echo πŸš€ k-rules Full-Stack Deploy echo ===================================================== echo. set ROOT=%~dp0 :: ──────────────────────────────────────────── :: [1] λ°±μ—”λ“œ λ¨Όμ € 배포 (API λ¨Όμ € μ€€λΉ„) :: ──────────────────────────────────────────── echo [1/2] Backend 배포 μ‹œμž‘... echo. cd /d %ROOT%backend call deploy.bat if errorlevel 1 ( echo [μ‹€νŒ¨] λ°±μ—”λ“œ 배포 μ‹€νŒ¨ - ν”„λ‘ νŠΈ 배포 쀑단 pause exit /b 1 ) :: ──────────────────────────────────────────── :: [2] ν”„λ‘ νŠΈμ—”λ“œ 배포 :: ──────────────────────────────────────────── echo. echo [2/2] Frontend 배포 μ‹œμž‘... echo. cd /d %ROOT%frontend call deploy.bat if errorlevel 1 ( echo [μ‹€νŒ¨] ν”„λ‘ νŠΈμ—”λ“œ 배포 μ‹€νŒ¨ pause exit /b 1 ) echo. echo ===================================================== echo βœ… ν’€μŠ€νƒ 배포 μ™„λ£Œ! echo 🌐 <https://xn--3e0b91t.com> echo ===================================================== pause
Shell
볡사
배포 μˆœμ„œ 팁: λ°±μ—”λ“œλ₯Ό λ¨Όμ € λ°°ν¬ν•˜λŠ” 게 μ•ˆμ „ν•΄μš”. ν”„λ‘ νŠΈκ°€ λ¨Όμ € μ˜¬λΌκ°€λ©΄ μ‚¬μš©μžκ°€ μƒˆλ‘œκ³ μΉ¨ν–ˆμ„ λ•Œ 이전 APIλ₯Ό ν˜ΈμΆœν•΄ μ—λŸ¬κ°€ λ‚  수 μžˆκ±°λ“ μš”!

OAuth μ†Œμ…œ 둜그인 톡합 ꡬ성

OAuth 인증 흐름

sequenceDiagram
    participant U as πŸ‘€ μ‚¬μš©μž
    participant F as πŸ“ React<br/>(κ΅­λ£°.com)
    participant N as πŸ–₯️ Nginx
    participant B as πŸƒ Spring Boot
    participant G as πŸ” Google OAuth

    U->>F: 1. ꡬ글 둜그인 λ²„νŠΌ 클릭
    F->>N: 2. GET /api/oauth2/authorization/google
    N->>B: 3. ν”„λ‘μ‹œ 전달 (location /api/ 블둝)
    B-->>N: 4. 302 Redirect to Google
    N-->>F: 5. λ¦¬λ‹€μ΄λ ‰νŠΈ 응닡
    F->>G: 6. ꡬ글 둜그인 νŽ˜μ΄μ§€ 이동

    U->>G: 7. ꡬ글 계정 둜그인 + λ™μ˜
    G-->>F: 8. Redirect to κ΅­λ£°.com/api/login/oauth2/code/google?code=xxx

    F->>N: 9. GET /api/login/oauth2/code/google?code=xxx
    N->>B: 10. ν”„λ‘μ‹œ 전달 (location /api/ 블둝, μΏ ν‚€ 포함)
    B->>G: 11. code둜 access_token μš”μ²­
    G-->>B: 12. access_token + μ‚¬μš©μž 정보
    B->>B: 13. νšŒμ›κ°€μž… or 둜그인 처리<br/>μ„Έμ…˜ μΏ ν‚€ λ°œκΈ‰
    B-->>N: 14. 302 Redirect to /
    N-->>F: 15. λ¦¬λ‹€μ΄λ ‰νŠΈ (μΏ ν‚€ 포함)
    F->>U: 16. 둜그인 μ™„λ£Œ ν™”λ©΄ βœ…
Mermaid
볡사

핡심 원리: μ™œ "같은 도메인"이 μ€‘μš”ν•œκ°€?

graph TD
    A["OAuth 콜백 URL<br/>κ΅­λ£°.com<br>/api/login/oauth2/code/google"] --> B{"ν”„λ‘ νŠΈμ—”λ“œμ™€<br/>같은 도메인?"}
    B -->|"βœ… Yes"| C["μΏ ν‚€ 곡유 OK<br/>CORS 문제 μ—†μŒ<br/>μ„Έμ…˜ μœ μ§€λ¨"]
    B -->|"❌ No"| D["μΏ ν‚€ SameSite 이슈<br/>CORS μ„€μ • 볡작<br/>둜그인 μ„Έμ…˜ λΆ„μ‹€"]

    style C fill:#90EE90
    style D fill:#FFB6C1
Mermaid
볡사
Nginx ν•˜λ‚˜λ‘œ ν•©μΉ˜λ©΄ λͺ¨λ“  λ¬Έμ œκ°€ ν•΄κ²°λΌμš”!
β€’
ν”„λ‘ νŠΈ(`κ΅­λ£°.com/`)와 λ°±μ—”λ“œ(`κ΅­λ£°.com/api/`, `κ΅­λ£°.com/api/oauth2/`)κ°€ 같은 도메인
β€’
μΏ ν‚€κ°€ μžμ—°μŠ€λŸ½κ²Œ 곡유됨 β†’ μ„Έμ…˜ 인증이 λ¬΄κ²°ν•˜κ²Œ λ™μž‘
β€’
CORS μ„€μ • μžμ²΄κ°€ ν•„μš” μ—†μŒ

μ™œ /api/login/oauth2/code/google인가?

일반적인 κ°€μ΄λ“œμ—μ„œλŠ” https://example.com/login/oauth2/code/google이라고 λ‚˜μ™€ μžˆμ–΄μš”.그런데 k-rules ν”„λ‘œμ νŠΈλŠ” /api/κ°€ λΆ™μ–΄μ•Ό ν•΄μš”. μ΄μœ κ°€ λ­˜κΉŒμš”?

핡심 이유: context-path: /api

Spring Boot에 context-path: /apiλ₯Ό μ„€μ •ν•˜λ©΄ λͺ¨λ“  μ—”λ“œν¬μΈνŠΈκ°€ /api/ ν•˜μœ„λ‘œ μ΄λ™ν•΄μš”. Spring Security OAuth2도 μ˜ˆμ™Έ 없이 μ μš©λ©λ‹ˆλ‹€.
graph LR
    subgraph "context-path μ—†λŠ” 경우 (일반)"
        A1["🟒 OAuth μ‹œμž‘<br/>/oauth2/authorization/google"]
        A2["🟒 OAuth 콜백<br/>/login/oauth2/code/google"]
        A3["🟒 REST API<br/>/products, /users"]
    end

    subgraph "context-path: /api 적용 ν›„ (k-rules)"
        B1["πŸ”΅ OAuth μ‹œμž‘<br/>/api/oauth2/authorization/google"]
        B2["πŸ”΅ OAuth 콜백<br/>/api/login/oauth2/code/google"]
        B3["πŸ”΅ REST API<br/>/api/products, /api/users"]
    end

    style B1 fill:#87CEEB
    style B2 fill:#87CEEB
    style B3 fill:#87CEEB
Mermaid
볡사
Nginx κ΄€μ μ—μ„œ 보면 이게 였히렀 κΉ”λ”ν•΄μš”!
graph TD
    R["πŸ“₯ Nginx μš”μ²­ μˆ˜μ‹ "] --> C1{"경둜 λΆ„κΈ°"}
    C1 -->|"/api/**<br/>(REST + OAuth μ „λΆ€!)"| SB["πŸƒ Spring Boot<br/>localhost:8080"]
    C1 -->|"κ·Έ μ™Έ"| REACT["πŸ“ React<br/>정적 파일"]

    SB --> E1["/api/products β†’ μƒν’ˆ API"]
    SB --> E2["/api/oauth2/authorization/google β†’ OAuth μ‹œμž‘"]
    SB --> E3["/api/login/oauth2/code/google β†’ OAuth 콜백 βœ…"]

    style SB fill:#87CEEB
    style REACT fill:#90EE90
Mermaid
볡사
context-path: /api 덕뢄에: location /api/ ν•˜λ‚˜λ‘œ REST API + OAuth μ‹œμž‘ + OAuth μ½œλ°±μ„ μ „λΆ€ 처리! λ³„λ„μ˜ /oauth2/, /login/oauth2/ location 블둝이 ν•„μš” μ—†μ–΄μ„œ Nginx 섀정이 더 λ‹¨μˆœν•΄μ Έμš”.

Spring Boot OAuth2 μ„€μ •

C:\\DEV\\k-rules\\backend\\src\\main\\resources\\application.yml:
spring: security: oauth2: client: registration: google: client-id: ${GOOGLE_CLIENT_ID} client-secret: ${GOOGLE_CLIENT_SECRET} scope: - profile - email # ⭐ context-path: /api μ΄λ―€λ‘œ /api/ κ°€ λΆ™μ–΄μ•Ό 함! redirect-uri: "https://xn--3e0b91t.com/api/login/oauth2/code/google" kakao: client-id: ${KAKAO_CLIENT_ID} client-secret: ${KAKAO_CLIENT_SECRET} redirect-uri: "<https://xn--3e0b91t.com/api/login/oauth2/code/kakao>" authorization-grant-type: authorization_code client-authentication-method: client_secret_post scope: - profile_nickname - account_email provider: kakao: authorization-uri: <https://kauth.kakao.com/oauth/authorize> token-uri: <https://kauth.kakao.com/oauth/token> user-info-uri: <https://kapi.kakao.com/v2/user/me> user-name-attribute: id
YAML
볡사

{baseUrl} 방식과 비ꡐ

Spring SecurityλŠ” redirect-uri에 {baseUrl} ν”Œλ ˆμ΄μŠ€ν™€λ”λ₯Ό 곡식 μ§€μ›ν•΄μš”. ν•˜λ“œμ½”λ”© λŒ€μ‹  μ•„λž˜μ²˜λŸΌ μ“Έ μˆ˜λ„ μžˆμ–΄μš”:
# βœ… {baseUrl} 방식 β€” ν™˜κ²½λ³„ 도메인이 μžλ™ 적용됨 spring: security: oauth2: client: registration: google: client-id: ${GOOGLE_CLIENT_ID} client-secret: ${GOOGLE_CLIENT_SECRET} scope: [profile, email] redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" # ↑ Spring Security κΈ°λ³Έκ°’! # context-path: /api 이면 {baseUrl} = https://xn--3e0b91t.com/api # β†’ μ‹€μ œ URL: https://xn--3e0b91t.com/api/login/oauth2/code/google βœ… kakao: client-id: ${KAKAO_CLIENT_ID} client-secret: ${KAKAO_CLIENT_SECRET} redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" authorization-grant-type: authorization_code client-authentication-method: client_secret_post scope: [profile_nickname, account_email] provider: kakao: authorization-uri: <https://kauth.kakao.com/oauth/authorize> token-uri: <https://kauth.kakao.com/oauth/token> user-info-uri: <https://kapi.kakao.com/v2/user/me> user-name-attribute: id
YAML
볡사
ν•­λͺ©
ν•˜λ“œμ½”λ”© 방식
{baseUrl} 방식
둜컬 개발
둜컬용 yml 별도 ν•„μš”
http://localhost:8080으둜 μžλ™ 인식
ν”„λ‘œλ•μ…˜
https://xn--3e0b91t.com λͺ…μ‹œ
Nginx ν”„λ‘μ‹œ 헀더 읽어 μžλ™ κ²°μ •
도메인 λ³€κ²½ μ‹œ
yml μˆ˜μ • ν•„μš”
μˆ˜μ • λΆˆν•„μš”
가독성
λͺ…ν™•ν•˜κ²Œ λ³΄μž„
좔상적이라 μ²˜μŒμ—” ν—·κ°ˆλ¦΄ 수 있음
μ§€μ›λ˜λŠ” ν”Œλ ˆμ΄μŠ€ν™€λ”:
λ³€μˆ˜
μ˜ˆμ‹œ κ°’
{baseUrl}
https://xn--3e0b91t.com/api (μŠ€ν‚΄ + 호슀트 + context-path 포함)
{baseScheme}
https
{baseHost}
xn--3e0b91t.com
{basePort}
:443 (ν‘œμ€€ 포트면 μƒλž΅λ¨)
{registrationId}
google, kakao, naver, ...
{baseUrl}이 μ˜¬λ°”λ₯΄κ²Œ λ™μž‘ν•˜λ €λ©΄ forward-headers-strategy: native λ˜λŠ” framework + Nginx의 X-Forwarded-Proto, X-Forwarded-Host 헀더 전달이 λ°˜λ“œμ‹œ ν•„μš”ν•΄μš”.
β€’
Spring이 λ‚΄λΆ€μ μœΌλ‘œ localhost:8080μ—μ„œ λŒμ§€λ§Œ, Nginxκ°€ μ „λ‹¬ν•œ 헀더 덕뢄에 {baseUrl}을 https://κ΅­λ£°.com/api둜 μΈμ‹ν•΄μš”.
β€’
context-path: /apiκ°€ 있으면 {baseUrl}에 /apiκ°€ μžλ™μœΌλ‘œ ν¬ν•¨λΌμš”!
β€’
κ·Έλž˜μ„œ
β—¦
{baseUrl}/login/oauth2/code/{registrationId}

OAuth 제곡자 μΈ‘ 콜백 URL 등둝

각 OAuth 제곡자의 개발자 μ½˜μ†”μ—μ„œ 승인된 λ¦¬λ‹€μ΄λ ‰μ…˜ URIλ₯Ό 등둝해야 ν•΄μš”.
제곡자
콜백 URL (ν”„λ‘œλ•μ…˜)
콜백 URL (둜컬)
Google
https://xn--3e0b91t.com/login/oauth2/code/google
http://localhost:8080/login/oauth2/code/google
Kakao
https://xn--3e0b91t.com/login/oauth2/code/kakao
http://localhost:8080/login/oauth2/code/kakao
Naver
https://xn--3e0b91t.com/login/oauth2/code/naver
http://localhost:8080/login/oauth2/code/naver
GitHub
https://xn--3e0b91t.com/login/oauth2/code/github
http://localhost:8080/login/oauth2/code/github
ν•œκΈ€ 도메인(κ΅­λ£°.com)이 μ•„λ‹Œ Punycode(xn--3e0b91t.com)둜 λ“±λ‘ν•˜μ„Έμš”! OAuth μ œκ³΅μžλ“€μ€ λŒ€λΆ€λΆ„ ν•œκΈ€ 도메인을 직접 λ°›μ§€ μ•Šμ•„μš”.

React 둜그인 λ²„νŠΌ κ΅¬ν˜„

F:\\DEV\\k-rules\\frontend\\src\\services\\memberService.js:
// VITE_API_URL μ—μ„œ /api μ ‘λ―Έλ₯Ό μ œκ±°ν•΄ 호슀트만 μΆ”μΆœ // 예) <http://localhost:8080/api> β†’ <http://localhost:8080> // <https://xn--3e0b91t.com/api> β†’ <https://xn--3e0b91t.com> export const getBackendBaseUrl = () => { const base = import.meta.env.VITE_API_URL || '<http://localhost:8080/api>'; return base.replace(/\\/api\\/?$/, ''); }; // μ†Œμ…œ 둜그인 μ‹œμž‘: λ°±μ—”λ“œ /api/oauth2/authorization/{provider} 둜 이동 // context-path: /api μ΄λ―€λ‘œ /api/ 접두사가 λ°˜λ“œμ‹œ ν•„μš”! export const startSocialLogin = (provider) => { const host = getBackendBaseUrl(); window.location.href = `${host}/api/oauth2/authorization/${provider}`; }; // 이미 둜그인된 μƒνƒœμ—μ„œ μ†Œμ…œ 계정 연동 μ‹œμž‘ export const startSocialLink = async (provider) => { const host = getBackendBaseUrl(); await api.post(`/me/social/link-init/${provider}`, {}, { withCredentials: true }); window.location.href = `${host}/api/oauth2/authorization/${provider}`; };
JavaScript
볡사
// LoginPage.jsx import { startSocialLogin } from '../services/memberService'; function LoginPage() { return ( <> <button onClick={() => startSocialLogin('google')}>κ΅¬κΈ€λ‘œ 둜그인</button> <button onClick={() => startSocialLogin('kakao')}>카카였둜 둜그인</button> </> ); }
JavaScript
볡사
μ™œ getBackendBaseUrl()둜 ν•œ 번 λ²—κ²¨λ‚΄λ‚˜μš”?
VITE_API_URL은 REST API용 base URL(/api 포함)μ΄μ—μš”. OAuth μ‹œμž‘ κ²½λ‘œλ„ /api/oauth2/...이라 같은 /api/ ν•˜μœ„μ΄κΈ΄ ν•œλ°, axios의 baseURL에 이미 /apiκ°€ λ“€μ–΄μžˆμ–΄μ„œ axios둜 ν˜ΈμΆœν•˜λ©΄ /api/api/...κ°€ λ˜μ–΄λ²„λ €μš”. κ·Έλž˜μ„œ 호슀트만 μΆ”μΆœ ν›„ μˆ˜λ™μœΌλ‘œ URL을 μ‘°λ¦½ν•˜λŠ” λ°©μ‹μ΄μ—μš”.
VITE_API_URL
getBackendBaseUrl() κ²°κ³Ό
OAuth μ‹œμž‘ URL
http://localhost:8080/api
http://localhost:8080
http://localhost:8080/api/oauth2/authorization/google
https://xn--3e0b91t.com/api
https://xn--3e0b91t.com
https://xn--3e0b91t.com/api/oauth2/authorization/google

Spring Boot 둜그인 성곡 ν›„ ν”„λ‘ νŠΈλ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈ

// C:\\DEV\\k-rules\\backend\\src\\main\\java\\...\\OAuth2SuccessHandler.java @Component public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { // 둜그인 ν›„ ν”„λ‘ νŠΈμ˜ νŠΉμ • νŽ˜μ΄μ§€λ‘œ 이동 String targetUrl = "/login/success"; // μƒλŒ€ 경둜! Nginxκ°€ μ•Œμ•„μ„œ 처리 getRedirectStrategy().sendRedirect(request, response, targetUrl); } }
Java
볡사
// SecurityConfig.java http .oauth2Login(oauth -> oauth .successHandler(oAuth2SuccessHandler) .failureUrl("/login?error=oauth") );
Java
볡사

ν™˜κ²½λ³€μˆ˜ 관리 (μ„œλ²„)

민감 μ •λ³΄λŠ” μ„œλ²„μ˜ ν™˜κ²½λ³€μˆ˜λ‘œ κ΄€λ¦¬ν•΄μš”. start.sh μˆ˜μ •:
#!/bin/bash cd "$(dirname "$0")" LOG_DIR="../log" mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/appwar_$(date +%Y%m%d_%H%M%S).log" JAVA_CMD=${JAVA_HOME:-}/bin/java if [ ! -x "$JAVA_CMD" ]; then JAVA_CMD=java fi JAVA_OPTS="-Xms128m -Xmx256m" # ⭐ OAuth ν™˜κ²½λ³€μˆ˜ (별도 νŒŒμΌμ—μ„œ λ‘œλ“œ) if [ -f ./env.sh ]; then source ./env.sh fi $JAVA_CMD $JAVA_OPTS -jar APP.war > "$LOG_FILE" 2>&1 &
Bash
볡사
env.sh (μ„œλ²„μ—λ§Œ 두고 gitμ—λŠ” μ˜¬λ¦¬μ§€ λ§ˆμ„Έμš”! .gitignore ν•„μˆ˜):
#!/bin/bash export GOOGLE_CLIENT_ID="xxxxx.apps.googleusercontent.com" export GOOGLE_CLIENT_SECRET="xxxxx" export KAKAO_CLIENT_ID="xxxxx" export KAKAO_CLIENT_SECRET="xxxxx" export SPRING_PROFILES_ACTIVE=prod
Bash
볡사
# μ„œλ²„μ—μ„œ env.sh 파일 보호 chmod 600 /var/www/krules/backend/env.sh
Bash
볡사

운영 ν™˜κ²½ 체크리슀트

HTTPS & λ³΄μ•ˆ

# Let's Encrypt μΈμ¦μ„œ λ°œκΈ‰ sudo certbot --nginx -d xn--3e0b91t.com -d www.xn--3e0b91t.com # HTTP β†’ HTTPS λ¦¬λ‹€μ΄λ ‰νŠΈλŠ” certbot이 μžλ™ μΆ”κ°€ # μžλ™ κ°±μ‹  확인 sudo certbot renew --dry-run
Bash
볡사

Nginx μ΅œμ’… μ„€μ • (HTTPS + λ³΄μ•ˆ 헀더)

upstream krules_backend { server localhost:8080; keepalive 32; } # HTTP β†’ HTTPS λ¦¬λ‹€μ΄λ ‰νŠΈ server { listen 80; server_name xn--3e0b91t.com www.xn--3e0b91t.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name xn--3e0b91t.com www.xn--3e0b91t.com; ssl_certificate /etc/letsencrypt/live/xn--3e0b91t.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/xn--3e0b91t.com/privkey.pem; # ⭐ λ³΄μ•ˆ 헀더 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; root /var/www/krules/frontend; index index.html; location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } # API + OAuth 곡톡 ν”„λ‘μ‹œ μ„€μ • location ~ ^/(api|oauth2|login/oauth2)/ { proxy_pass http://krules_backend; 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_set_header X-Forwarded-Host $host; proxy_http_version 1.1; # OAuth μ„Έμ…˜ μΏ ν‚€ 보호 proxy_cookie_path / "/; Secure; SameSite=Lax"; proxy_connect_timeout 60s; proxy_read_timeout 60s; client_max_body_size 20M; } location / { try_files $uri $uri/ /index.html; } gzip on; gzip_types text/plain text/css application/json application/javascript text/xml; }
Plain Text
볡사

전체 배포 흐름 μš”μ•½

flowchart TD
    A["πŸ’» 둜컬: <br/>deploy-all.bat 더블클릭"] --> B["πŸ”¨ Backend: <br/>gradlew clean bootWar"]
    B --> C["πŸ“€ APP.war + *.sh μ—…λ‘œλ“œ"]
    C --> D["πŸ”„ restart.sh 원격 μ‹€ν–‰"]
    D --> E["πŸ”¨ Frontend: <br/>npm run build"]
    E --> F["πŸ“€ dist/* μ—…λ‘œλ“œ"]
    F --> G{"Nginx λΆ„κΈ°"}
    G -->|"/api/"| H["πŸƒ Spring Boot"]
    G -->|"/oauth2/<br/>/login/oauth2/"| H
    G -->|"/ (κ·Έ μ™Έ)"| I["πŸ“ React SPA"]
    H --> J["🌐 κ΅­λ£°.com μ„œλΉ„μŠ€ 쀑!"]
    I --> J

    style A fill:#FFD700
    style J fill:#90EE90
Mermaid
볡사

ν’€μŠ€νƒ 배포 체크리슀트

사전 μ€€λΉ„

~/.ssh/config 에 alohaserver4 Host μ„€μ • μ™„λ£Œ?
μ„œλ²„μ— /var/www/krules/{frontend,backend,log} 디렉토리 생성?
λ°±μ—”λ“œ start.sh / stop.sh / restart.sh ν”„λ‘œμ νŠΈ 루트 쑴재?
λ°±μ—”λ“œ env.sh 생성 및 chmod 600 적용?

ν”„λ‘ νŠΈμ—”λ“œ (C:\\DEV\\k-rules\\frontend)

.env.local 에 VITE_API_URL=http://localhost:8080/api μ„€μ •?
.env.production 에 VITE_API_URL=/api μ„€μ •?
npm run build β†’ dist/ 생성 확인?
deploy.bat λ™μž‘ 확인?

λ°±μ—”λ“œ (C:\\DEV\\k-rules\\backend)

application.yml에 forward-headers-strategy: framework μ„€μ •?
OAuth redirect-uriκ°€ ν”„λ‘œλ•μ…˜ λ„λ©”μΈμœΌλ‘œ μ„€μ •?
./gradlew clean bootWar β†’ build/libs/APP.war 생성?
deploy.bat λ™μž‘ 확인?

OAuth 제곡자

Google Cloud Console에 콜백 URL 등둝 (ν”„λ‘œλ•μ…˜ + 둜컬)?
Kakao Developers에 Redirect URI 등둝?
Client ID / Secret을 μ„œλ²„ env.sh에 μ„€μ •?

Nginx

/etc/nginx/conf.d/krules.conf μ„€μ • μ™„λ£Œ?
/api/, /oauth2/, /login/oauth2/ λͺ¨λ‘ λ°±μ—”λ“œλ‘œ ν”„λ‘μ‹œ?
try_files κ°€ location / 에 μ„€μ •λ˜μ–΄ SPA λΌμš°νŒ… λ™μž‘?
HTTPS μΈμ¦μ„œ 적용 (certbot --nginx)?
nginx -t 톡과 및 systemctl reload nginx μ™„λ£Œ?

톡합 ν…ŒμŠ€νŠΈ

λΈŒλΌμš°μ €μ—μ„œ https://xn--3e0b91t.com 접속 OK?
React λΌμš°νŒ…(/about λ“±) μƒˆλ‘œκ³ μΉ¨ μ‹œμ—λ„ 정상 λ™μž‘?
/api/... API 호좜 정상?
ꡬ글 OAuth 둜그인 β†’ 성곡 ν›„ ν”„λ‘ νŠΈλ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈ 확인?
둜그인 ν›„ μ„Έμ…˜ μœ μ§€(μƒˆλ‘œκ³ μΉ¨ 후에도 둜그인 μƒνƒœ)?
deploy-all.bat 톡합 배포 ν…ŒμŠ€νŠΈ μ™„λ£Œ?

핡심 정리

ν’€μŠ€νƒ ꡬ성 = Nginx ν•˜λ‚˜μ—μ„œ React(정적) + Spring Boot(동적) λ™μ‹œ μ„œλΉ™
location / β†’ React 정적 파일 (try_files둜 SPA λΌμš°νŒ…)
location /api/ β†’ Spring Boot λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ (proxy_pass)
같은 도메인이라 CORS 문제 μ—†μŒ β€” Reactμ—μ„œ /api/둜 λ°”λ‘œ 호좜
gzip μ••μΆ• + 정적 에셋 캐싱 + HTTPS 적용으둜 ν”„λ‘œλ•μ…˜ μ΅œμ ν™”