React와 Spring Boot 연동 구조: CORS, 프록시, 배포 전략
한눈에 보기
React SPA와 Spring Boot REST API 서버를 분리해 구성하는 아키텍처를 다룬다. 개발 환경의 CORS와 프록시 설정, 정적 빌드 산출물의 통합 서빙과 분리 서빙 전략, 새로고침 404 해결까지 원리 중심으로 설명한다.
최종 업데이트 2026.09.17
React(리액트)와 Spring Boot(스프링 부트) 연동은 화면을 담당하는 싱글 페이지 애플리케이션(Single Page Application)과 데이터를 담당하는 REST API 서버를 분리해 하나의 서비스를 구성하는 아키텍처를 뜻한다. 개발 단계에서는 두 서버가 서로 다른 포트에서 동작하므로 CORS(Cross-Origin Resource Sharing)와 프록시(proxy) 설정이 핵심 과제가 되고, 배포 단계에서는 정적 빌드 산출물을 어디서 서빙할지가 핵심 결정이 된다. 이 문서는 특정 버전의 설치 절차가 아니라 구조와 원리를 중심으로 연동 방식을 설명한다.
프론트엔드와 백엔드를 분리하는 이유
JSP나 Thymeleaf 같은 서버 사이드 템플릿 방식에서는 서버가 완성된 HTML을 만들어 내려보낸다. 반면 분리 구조에서는 React가 브라우저에서 화면을 그리고, Spring Boot는 HTML을 전혀 만들지 않은 채 JSON만 반환하는 API 서버 역할에 집중한다. 두 애플리케이션을 잇는 유일한 계약은 API 명세이므로, 프론트엔드와 백엔드 팀이 독립적으로 개발하고 배포할 수 있으며 같은 API를 모바일 앱 등 다른 클라이언트가 재사용할 수도 있다.
개발 환경: 서로 다른 포트와 CORS
개발 중에는 React 개발 서버(Vite 기준 5173 포트)와 Spring Boot(8080 포트)가 동시에 떠 있다. 브라우저는 동일 출처 정책(same-origin policy)에 따라 출처(origin)가 다른 요청의 응답 읽기를 기본적으로 차단하므로, 프론트엔드에서 API를 호출하면 CORS 오류가 발생한다. 해법은 두 가지다. 하나는 개발 서버의 프록시 기능으로 요청을 대신 전달하는 것이고, 다른 하나는 백엔드가 허용 출처를 응답 헤더로 명시하는 것이다.
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});
프록시 방식에서는 브라우저 입장에서 요청이 같은 출처로 나가고 서버 간 전달은 CORS의 적용 대상이 아니므로 문제가 아예 발생하지 않는다. 백엔드에서 허용하는 방식은 다음과 같다.
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
단순 요청이 아닌 경우 브라우저는 본 요청 전에 OPTIONS 메서드의 프리플라이트(preflight) 요청을 먼저 보내 허용 여부를 확인한다는 점도 알아 두어야 한다.
배포 전략: 통합 서빙과 분리 서빙
빌드 명령이 만들어 내는 산출물은 HTML, 자바스크립트, CSS로 이루어진 순수 정적 파일이다. 이 파일을 어디서 서빙할지에 따라 두 가지 전략으로 나뉜다.
| 구분 | 통합 배포 | 분리 배포 |
|---|---|---|
| 구성 | 빌드 산출물을 Spring Boot의 정적 리소스 경로에 넣어 단일 jar로 배포한다 | 정적 파일은 Nginx나 CDN이, API는 Spring Boot가 각각 서빙한다 |
| CORS | 같은 출처이므로 발생하지 않는다 | 도메인이 다르면 CORS 설정이 필요하다 |
| 배포 주기 | 프론트엔드와 백엔드가 항상 함께 배포된다 | 서로 독립적으로 배포할 수 있다 |
| 성능 확장 | 정적 파일 요청까지 애플리케이션 서버가 감당한다 | 정적 파일 캐싱과 API 스케일링을 따로 최적화할 수 있다 |
| 적합한 규모 | 소규모 서비스, 사내 관리 도구 | 트래픽이 크거나 팀이 분리된 서비스 |
SPA 라우팅과 새로고침 404 문제
React Router가 히스토리 API 기반의 실제 경로 형태 URL(/users/3 등)을 사용하면, 해당 화면에서 새로고침했을 때 서버에는 그 경로에 대응하는 파일이 없으므로 404가 발생한다. 라우팅 정보가 서버가 아니라 브라우저의 자바스크립트에만 존재하기 때문이다. 해결책은 서버가 알 수 없는 경로 요청을 index.html로 되돌려 주는 폴백(fallback) 설정이다. 통합 배포라면 Spring에서 매칭되지 않는 경로를 index.html로 포워딩하는 설정을 두고, 분리 배포라면 Nginx의 try_files 지시어 같은 기능을 쓴다. 이때 API 경로는 폴백 대상에서 제외해야 클라이언트가 JSON 대신 HTML을 받는 혼란을 막을 수 있다.
주의할 점
- 프론트엔드 빌드에 포함된 환경 변수 값은 모두 사용자에게 노출된다. API 주소 같은 공개 정보만 넣고 비밀 키는 절대 넣지 않는다.
- 인증에 쿠키를 쓰는 분리 배포 구성에서는 쿠키의 SameSite 속성, 서버의 allowCredentials, 프론트엔드 요청의 credentials 옵션을 함께 맞춰야 한다.
- 개발 서버의 프록시는 개발 편의 도구일 뿐이므로 운영 환경 구성은 별도로 설계해야 한다.
- API 경로에 /api 같은 공통 접두사를 두면 프록시, 폴백, 게이트웨이 설정이 모두 단순해진다.
면접에서 이렇게 물어본다
- CORS는 왜 발생하며, 프론트엔드 프록시와 백엔드 설정 중 어느 쪽에서 푸는 것이 어떤 상황에 적합한가.
- SPA에서 새로고침 시 404가 발생하는 이유와 해결 방법을 설명해 보라.
- 정적 빌드를 백엔드 jar에 포함하는 방식과 CDN에서 서빙하는 방식의 장단점을 비교해 보라.
- 프리플라이트 요청은 언제 발생하고 서버는 어떻게 응답해야 하는가.
- 프론트엔드 환경 변수에 비밀 값을 넣으면 안 되는 이유는 무엇인가.
React / Vue에서 같이 읽기
- React Fragment: 불필요한 래퍼 없이 요소를 묶는 방법React Fragment는 여러 자식 요소를 추가 DOM 노드 없이 하나로 묶어 반환하게 해 주는 문법 요소다. 하나의 루트 요소 제약을 래퍼 없이 해결해 DOM 깊이와 CSS 레이아웃 문제를 예방하는 원리와 key 사용법을 정리한다.
- React Hook: 함수 컴포넌트의 상태와 로직 재사용React Hook은 함수 컴포넌트에서 상태 관리와 사이드 이펙트 처리를 가능하게 하는 함수들의 모음이다. 훅의 등장 배경, 호출 순서 기반 동작 원리와 규칙, 커스텀 훅을 통한 로직 재사용까지 면접 관점에서 정리한다.
- Vue 3와 Firebase 이메일 인증: 토큰 기반 인증 흐름Firebase Authentication은 이메일 인증을 서버 구현 없이 제공하는 관리형 인증 서비스다. ID 토큰과 리프레시 토큰의 흐름, Vue 3 컴포저블 통합, 라우터 가드와 세션 유지, 보안 주의점을 원리 중심으로 정리한다.
- Vue 3와 Firebase 소셜 로그인: OAuth 위임과 연동 원리소셜 로그인의 바탕인 OAuth 2.0 권한 위임 원리와 Firebase가 소셜 제공자를 중개하는 인증 흐름을 다룬다. 팝업과 리다이렉트 방식 비교, Vue 3 구현, 계정 충돌 처리까지 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기