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:#87CEEBMermaid
볡μ¬
μμ² νλ¦ μμΈ
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:#FFD700Mermaid
볡μ¬
ν΅μ¬ μ€μ | μ€λͺ
| μ νμ? |
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://λ‘ μ¬λ°λ₯΄κ² μμ±ν΄μ.
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:#FFB6C1Mermaid
볡μ¬
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:#87CEEBMermaid
볡μ¬
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:#90EE90Mermaid
볡μ¬
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
볡μ¬
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:#90EE90Mermaid
볡μ¬
νμ€ν λ°°ν¬ μ²΄ν¬λ¦¬μ€νΈ
μ¬μ μ€λΉ
~/.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 μ μ©μΌλ‘ νλ‘λμ
μ΅μ ν




