Spring Boot와 React 배포 방식
in Backend / Spring on React, Spring boot, 배포, 정적 리소스, 보안, 라우팅, Gradle
Spring Boot와 React 배포 방식
목차
1) 동작 구조
- 배포 시점:
npm run build가 실행되면서 정적 파일이 생성됩니다.- 생성된 파일은
src/main/resources/static/에 위치합니다. ./gradlew bootJar또는mvn package명령어로 JAR/WAR가 생성됩니다.
- 런타임:
- Spring Boot는
/index.html,/assets/...경로를 통해 정적 파일을 서빙합니다. - API 요청은
/api/**형태로 Spring MVC에 의해 처리됩니다.
- Spring Boot는
이 방식은 Vite dev server를 운영 환경에서 사용하지 않겠다는 요구사항을 만족하는 방법입니다.
2) 정적 리소스 위치
Spring Boot의 기본 정적 리소스 경로는 일반적으로 다음과 같습니다.
classpath:/static/classpath:/public/classpath:/resources/classpath:/META-INF/resources/
실무에서는 대개 src/main/resources/static/ 안에 React 빌드 결과를 넣는 것이 일반적입니다.
예시:
src/main/resources/static/index.htmlsrc/main/resources/static/assets/*
3) SPA 라우팅 처리(중요)
React Router의 History 모드를 사용할 경우, /user/list 등의 경로로 직접 접속할 때 Spring이 404 오류를 발생시킬 수 있습니다. 이 경우에는 정적 파일 요청이 아닌 경로에 대해 index.html로 포워딩해야 합니다. (단, /api/**는 제외)
가장 흔한 방식: WebMvcConfigurer로 forward
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SpaForwardConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 1-depth
registry.addViewController("/{spring:[a-zA-Z0-9-_]+}")
.setViewName("forward:/index.html");
// multi-depth
registry.addViewController("/**/{spring:[a-zA-Z0-9-_]+}")
.setViewName("forward:/index.html");
// 파일 확장자 있는 요청은 제외
registry.addViewController("/{spring:[a-zA-Z0-9-_]+}/**{spring:?!(\\.js|\\.css|\\.png|\\.jpg|\\.svg|\\.ico)$}")
.setViewName("forward:/index.html");
}
}
API 라우팅은 /api/**로 설정하여 API 요청이 이 규칙을 가로채지 않도록 하는 것이 좋습니다.
4) Spring Security 쓰는 경우(정적 리소스 허용)
정적 파일이 인증에 의해 차단되면 화면이 제대로 표시되지 않습니다. 따라서 최소한 아래와 같은 리소스는 permitAll()로 설정해야 합니다.
http.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/", "/index.html",
"/assets/**",
"/*.js", "/*.css",
"/favicon.ico"
).permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);
정확한 패턴은 빌드 결과물의 경로에 따라 조정이 필요합니다.
5) 빌드 파이프라인에서 “자동으로 복사”시키기
Gradle 예시(멀티 모듈/단일 모듈 공통 아이디어)
프론트엔드가 frontend/에 위치하고 빌드 결과물이 frontend/dist라고 가정합니다.
tasks.register("buildFrontend", Exec) {
workingDir = file("frontend")
commandLine "npm", "ci"
}
tasks.register("buildFrontendAssets", Exec) {
dependsOn("buildFrontend")
workingDir = file("frontend")
commandLine "npm", "run", "build"
}
tasks.register("copyFrontend", Copy) {
dependsOn("buildFrontendAssets")
from("frontend/dist")
into("$buildDir/resources/main/static")
}
processResources {
dependsOn("copyFrontend")
}
이렇게 하면 bootJar 시점에 프론트 빌드 결과가 JAR에 포함됩니다.
Maven도 동일하게 가능
frontend-maven-plugin또는 exec/copy 작업을 통해process-resources전에 프론트 빌드 및 복사가 가능합니다.
6) 이 방식의 장단점
장점
- 서버 1대에서 운영 가능, 배포가 간단합니다.
- 동일 도메인 덕분에 CORS 문제를 최소화합니다.
- 운영 환경에서 Node 또는 Vite 서버가 필요하지 않습니다.
단점/주의
- 프론트엔드만 별도로 롤링 배포하기 어려우며 백엔드와 패키징이 결합됩니다.
- SPA 라우팅 포워딩 설정이 필수적입니다.
- 캐시 정책을 신경 쓰지 않으면 배포 후 갱신 문제가 발생할 수 있습니다.