React + Spring Boot 풀스택 개발환경 구성 매뉴얼

React + Spring Boot 풀스택 환경 구성

FE/BE 분리 구조로 개발부터 배포까지 — 완전 가이드

React 18 Spring Boot 3.x Docker

전체 아키텍처

브라우저
React App
⟷ HTTP/JSON ⟷
Spring Boot
REST API
⟷ JDBC/JPA ⟷
Database
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 파이프라인 연동
  • 헬스체크 자동화

사전 준비

개발 환경 구성 전 필수 설치 항목

필수 소프트웨어

  • 1
    JDK 17 이상 설치 (LTS 권장)
    winget install Microsoft.OpenJDK.17
    java -version
    JAVA_HOME 환경변수 설정 필수. C:\Program Files\Microsoft\jdk-17
  • 2
    Node.js 20 LTS 설치
    winget install OpenJS.NodeJS.LTS
    node -v   # v20.x.x
    npm -v    # 10.x.x
  • 3
    IDE 설치

    BE: IntelliJ IDEA

    • Community 무료 사용 가능
    • Spring Boot 플러그인 내장
    • Gradle/Maven 자동 인식

    FE: VS Code

    • ESLint / Prettier 확장
    • ES7+ React Snippets
    • GitLens 추천
  • 4
    Docker 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 구현까지

프로젝트 생성
CORS 설정
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

React 프론트엔드 설정

Vite 기반 React 앱 구성 및 API 연동

프로젝트 생성
Proxy 설정
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

데이터베이스 연동

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             # 종료

배포 전략

개발 → 스테이징 → 프로덕션 워크플로우

개발 환경 실행 순서

  • 1
    DB 실행
    docker run -d --name mysql-dev -e MYSQL_ROOT_PASSWORD=password \
      -e MYSQL_DATABASE=mydb -p 3306:3306 mysql:8.0
  • 2
    Spring Boot 실행
    cd backend
    ./gradlew bootRun --args='--spring.profiles.active=dev'
    # http://localhost:8080/swagger-ui.html 에서 API 확인
  • 3
    React 개발 서버 실행
    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)