React + Spring Boot 풀스택 환경 구성
FE/BE 분리 구조로 개발부터 배포까지 — 완전 가이드
React 18
Spring Boot 3.x
Docker
전체 아키텍처
브라우저
React App
React App
⟷ HTTP/JSON ⟷
Spring Boot
REST API
REST API
⟷ JDBC/JPA ⟷
Database
MySQL / PostgreSQL
MySQL / PostgreSQL
:3000 (dev)
→ proxy →
:8080
→
:3306 / :5432
주요 특징
완전 분리 구조
- FE/BE 독립 빌드·배포
- 팀 분업 용이
- 각자 다른 기술 스택 사용 가능
- 개발 서버 핫 리로드 지원
REST API 통신
- JSON 기반 데이터 교환
- Swagger UI로 API 문서 자동화
- 개발 중 Proxy로 CORS 우회
- 프로덕션은 Nginx 리버스 프록시
보안 구조
- Spring Security + JWT 인증
- CORS 화이트리스트 관리
- 환경변수로 민감정보 분리
- HTTPS 적용 (배포 환경)
컨테이너 배포
- Docker Compose로 통합 관리
- 환경별 설정 분리 (dev/prod)
- CI/CD 파이프라인 연동
- 헬스체크 자동화
사전 준비
개발 환경 구성 전 필수 설치 항목
필수 소프트웨어
-
1JDK 17 이상 설치 (LTS 권장)
winget install Microsoft.OpenJDK.17 java -version
JAVA_HOME 환경변수 설정 필수.C:\Program Files\Microsoft\jdk-17 -
2Node.js 20 LTS 설치
winget install OpenJS.NodeJS.LTS node -v # v20.x.x npm -v # 10.x.x
-
3IDE 설치
BE: IntelliJ IDEA
- Community 무료 사용 가능
- Spring Boot 플러그인 내장
- Gradle/Maven 자동 인식
FE: VS Code
- ESLint / Prettier 확장
- ES7+ React Snippets
- GitLens 추천
-
4Docker Desktop 설치 (선택, 권장)
winget install Docker.DockerDesktop docker -v && docker compose version
추천 VS Code 확장
ES7+ React/Redux/React-Native snippets Prettier - Code formatter ESLint Auto Import GitLens Thunder Client # REST API 테스트 (Postman 대체)
Spring Boot 백엔드 설정
프로젝트 생성부터 REST API 구현까지
1. Spring Initializr로 프로젝트 생성
start.spring.io 에서 아래 옵션 선택
Project: Gradle - Groovy (또는 Maven) Language: Java Spring Boot: 3.2.x Dependencies: ✓ Spring Web ✓ Spring Data JPA ✓ Spring Security (선택) ✓ MySQL Driver (또는 PostgreSQL Driver) ✓ Lombok ✓ Validation ✓ springdoc-openapi (Swagger)
2. application.yml 기본 설정
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&characterEncoding=UTF-8
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:password}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # prod에서는 validate 또는 none
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQLDialect
mvc:
pathmatch:
matching-strategy: ant_path_matcher
3. build.gradle 주요 의존성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
implementation 'com.mysql:mysql-connector-j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
CORS 설정 (WebMvcConfigurer)
개발 중에는 React 개발 서버(3000)를 허용해야 API 호출이 가능합니다.
// config/CorsConfig.java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(
"http://localhost:3000", // React 개발 서버
"https://yourdomain.com" // 프로덕션 도메인
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
Spring Security 사용 시 추가 설정
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
Entity + Repository + Service + Controller 패턴
// entity/Item.java
@Entity
@Table(name = "items")
@Getter @Setter @NoArgsConstructor
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String description;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
}
// repository/ItemRepository.java
public interface ItemRepository extends JpaRepository<Item, Long> {
List<Item> findByNameContaining(String keyword);
}
// service/ItemService.java
@Service @RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
public List<Item> findAll() { return itemRepository.findAll(); }
public Item findById(Long id) {
return itemRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Item not found"));
}
public Item save(Item item) { return itemRepository.save(item); }
public void delete(Long id) { itemRepository.deleteById(id); }
}
// controller/ItemController.java
@RestController
@RequestMapping("/api/items")
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping
public ResponseEntity<List<Item>> getAll() {
return ResponseEntity.ok(itemService.findAll());
}
@GetMapping("/{id}")
public ResponseEntity<Item> getOne(@PathVariable Long id) {
return ResponseEntity.ok(itemService.findById(id));
}
@PostMapping
public ResponseEntity<Item> create(@RequestBody @Valid Item item) {
return ResponseEntity.status(HttpStatus.CREATED).body(itemService.save(item));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
itemService.delete(id);
return ResponseEntity.noContent().build();
}
}
Swagger UI 접근:
http://localhost:8080/swagger-ui.html권장 디렉토리 구조
backend/
├── src/main/java/com/example/app/
│ ├── config/# 설정 클래스 (CORS, Security, Swagger)
│ ├── controller/# REST 컨트롤러
│ ├── dto/# 요청/응답 DTO
│ ├── entity/# JPA Entity
│ ├── repository/# JPA Repository
│ ├── service/# 비즈니스 로직
│ ├── exception/# 예외 처리
│ └── Application.java
├── src/main/resources/
│ ├── application.yml
│ ├── application-dev.yml
│ └── application-prod.yml
└── build.gradle
├── src/main/java/com/example/app/
│ ├── config/# 설정 클래스 (CORS, Security, Swagger)
│ ├── controller/# REST 컨트롤러
│ ├── dto/# 요청/응답 DTO
│ ├── entity/# JPA Entity
│ ├── repository/# JPA Repository
│ ├── service/# 비즈니스 로직
│ ├── exception/# 예외 처리
│ └── Application.java
├── src/main/resources/
│ ├── application.yml
│ ├── application-dev.yml
│ └── application-prod.yml
└── build.gradle
React 프론트엔드 설정
Vite 기반 React 앱 구성 및 API 연동
Vite로 React 프로젝트 생성 (권장)
npm create vite@latest frontend -- --template react cd frontend npm install
주요 패키지 설치
# 필수 npm install axios # HTTP 클라이언트 npm install react-router-dom # 라우팅 npm install @tanstack/react-query # 서버 상태 관리 # 선택 (UI 라이브러리) npm install @mui/material @emotion/react @emotion/styled # MUI # 또는 npm install antd # Ant Design # 또는 npm install -D tailwindcss postcss autoprefixer # Tailwind CSS # 개발 도구 npm install -D eslint prettier npm install -D @typescript-eslint/parser # TypeScript 사용 시
TypeScript 권장:
npm create vite@latest frontend -- --template react-ts환경변수 설정 (.env)
# .env.development VITE_API_BASE_URL=http://localhost:8080/api # .env.production VITE_API_BASE_URL=https://api.yourdomain.com/api
개발 중 CORS 우회 — Vite Proxy 설정
Proxy를 사용하면 React에서 Spring Boot로 직접 요청하는 것처럼 보이게 해 CORS 문제를 우회합니다.
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, '') // 경로 제거 시
}
}
},
resolve: {
alias: {
'@': '/src' // 절대 경로 import 사용: import Foo from '@/components/Foo'
}
}
})
개발 환경
- React 앱: localhost:3000
- /api/* → localhost:8080
- Vite가 프록시 처리
- CORS 설정 불필요 (선택)
프로덕션 환경
- React 빌드 → /dist
- Nginx가 정적 파일 서빙
- /api/* → Spring Boot로 리버스 프록시
- 단일 도메인으로 운영
Axios 인스턴스 설정
// src/api/axiosInstance.js
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
})
// 요청 인터셉터 (토큰 자동 첨부)
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// 응답 인터셉터 (에러 공통 처리)
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default api
React Query로 데이터 페칭
// hooks/useItems.js
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from '@/api/axiosInstance'
export const useItems = () =>
useQuery({
queryKey: ['items'],
queryFn: () => api.get('/items').then(res => res.data)
})
export const useCreateItem = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (item) => api.post('/items', item),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] })
})
}
// 컴포넌트에서 사용
function ItemList() {
const { data: items, isLoading, error } = useItems()
if (isLoading) return <div>로딩 중...</div>
if (error) return <div>오류 발생</div>
return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
}
권장 디렉토리 구조
frontend/
├── src/
│ ├── api/# axios 인스턴스, API 함수
│ ├── components/# 재사용 UI 컴포넌트
│ │ ├── common/# Button, Input, Modal...
│ │ └── layout/# Header, Sidebar, Footer
│ ├── pages/# 라우트별 페이지 컴포넌트
│ ├── hooks/# 커스텀 훅 (useItems 등)
│ ├── store/# 전역 상태 (Zustand/Redux)
│ ├── utils/# 유틸 함수
│ ├── types/# TypeScript 타입 정의
│ ├── App.jsx
│ └── main.jsx
├── .env.development
├── .env.production
└── vite.config.js
├── src/
│ ├── api/# axios 인스턴스, API 함수
│ ├── components/# 재사용 UI 컴포넌트
│ │ ├── common/# Button, Input, Modal...
│ │ └── layout/# Header, Sidebar, Footer
│ ├── pages/# 라우트별 페이지 컴포넌트
│ ├── hooks/# 커스텀 훅 (useItems 등)
│ ├── store/# 전역 상태 (Zustand/Redux)
│ ├── utils/# 유틸 함수
│ ├── types/# TypeScript 타입 정의
│ ├── App.jsx
│ └── main.jsx
├── .env.development
├── .env.production
└── vite.config.js
데이터베이스 연동
MySQL / PostgreSQL 로컬 설치 및 Docker 실행
Docker로 DB 빠르게 실행
# MySQL docker run -d \ --name mysql-dev \ -e MYSQL_ROOT_PASSWORD=password \ -e MYSQL_DATABASE=mydb \ -p 3306:3306 \ mysql:8.0 # PostgreSQL docker run -d \ --name postgres-dev \ -e POSTGRES_PASSWORD=password \ -e POSTGRES_DB=mydb \ -p 5432:5432 \ postgres:15
JPA Entity 기본 설정
@Entity
@Table(name = "items")
@Getter @Setter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
// Application.java에 추가
@SpringBootApplication
@EnableJpaAuditing // createdAt, updatedAt 자동 관리
public class Application { ... }
글로벌 예외 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<Map<String, String>> handleNotFound(EntityNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors()
.stream().map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(Map.of("error", message));
}
}
Docker Compose 통합 구성
FE + BE + DB를 한 번에 실행하는 개발/배포 환경
Dockerfile — Spring Boot
# backend/Dockerfile FROM eclipse-temurin:17-jdk-alpine AS builder WORKDIR /app COPY gradlew . COPY gradle gradle COPY build.gradle settings.gradle . COPY src src RUN chmod +x gradlew && ./gradlew build -x test FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=builder /app/build/libs/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
Dockerfile — React (Nginx 서빙)
# frontend/Dockerfile FROM node:20-alpine AS builder WORKDIR /app COPY package*.json . RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
nginx.conf (SPA 라우팅 + API 프록시)
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html; # SPA 라우팅
}
location /api/ {
proxy_pass http://backend:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: myapp-db
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: mydb
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
retries: 5
backend:
build: ./backend
container_name: myapp-be
ports:
- "8080:8080"
environment:
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/mydb
depends_on:
mysql:
condition: service_healthy
frontend:
build: ./frontend
container_name: myapp-fe
ports:
- "80:80"
depends_on:
- backend
volumes:
mysql-data:
# 실행 명령 docker compose up -d --build # 전체 실행 docker compose logs -f backend # 로그 확인 docker compose down # 종료
배포 전략
개발 → 스테이징 → 프로덕션 워크플로우
개발 환경 실행 순서
-
1DB 실행
docker run -d --name mysql-dev -e MYSQL_ROOT_PASSWORD=password \ -e MYSQL_DATABASE=mydb -p 3306:3306 mysql:8.0
-
2Spring Boot 실행
cd backend ./gradlew bootRun --args='--spring.profiles.active=dev' # http://localhost:8080/swagger-ui.html 에서 API 확인
-
3React 개발 서버 실행
cd frontend npm run dev # http://localhost:3000 에서 FE 확인
GitHub Actions CI/CD 예시
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build Spring Boot
run: |
cd backend
./gradlew build -x test
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build React
run: |
cd frontend
npm ci && npm run build
env:
VITE_API_BASE_URL: ${{ secrets.API_BASE_URL }}
- name: Deploy via Docker Compose
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /opt/myapp
docker compose pull
docker compose up -d --build
민감 정보(DB 비밀번호, JWT 시크릿)는 반드시 GitHub Secrets 또는 서버 .env 파일로 관리하세요.
구성 완료 체크리스트
개발 환경 구성 전 과정 확인
사전 준비
- JDK 17+ 설치 및 JAVA_HOME 설정
- Node.js 20 LTS 설치
- IntelliJ IDEA (Community 이상) 설치
- VS Code + 추천 확장 설치
- Docker Desktop 설치
Spring Boot 백엔드
- Spring Initializr로 프로젝트 생성 (Web, JPA, MySQL)
- application.yml 데이터소스 설정
- CORS 설정 (localhost:3000 허용)
- Entity / Repository / Service / Controller 작성
- GlobalExceptionHandler 작성
- Swagger UI 동작 확인 (:8080/swagger-ui.html)
- 환경별 application-dev.yml, application-prod.yml 분리
React 프론트엔드
- Vite + React (또는 TypeScript) 프로젝트 생성
- vite.config.js Proxy 설정 (/api → :8080)
- Axios 인스턴스 + 인터셉터 설정
- React Query 설정 (QueryClientProvider)
- .env.development / .env.production 작성
- ESLint + Prettier 설정
- React Router 기본 라우팅 구성
Docker / 배포
- backend/Dockerfile 작성 (멀티 스테이지 빌드)
- frontend/Dockerfile + nginx.conf 작성
- docker-compose.yml 작성 (healthcheck 포함)
- .env 파일 작성 및 .gitignore 등록
- docker compose up -d 로 전체 동작 확인
- GitHub Actions 또는 CI/CD 파이프라인 설정
- 프로덕션 HTTPS 적용 (Let's Encrypt + Nginx)