최근에 학교 학생 시스템의 정보를 파싱하는 웹 앱을 대대적인 개편을 하면서 리액트 프로젝트와 Spring 프로젝트를 통합시켰다.
이전 게시글에서도 간단하게 언급했었는데, 각기 다른 프로젝트였고 각기 다른 컨테이너에서 동작하다 보니 서브 도메인이 달랐다.
그래서 CORS 설정을 해결해야 했고, 세션 쿠키를 유지시키려고 야매 (?) 방법을 동원했다.
자 그럼 FE를 BE에 어떻게 통합할 수 있을까?
이 방식은 FE와 BE의 깃헙 레포가 다르더라도 크게 문제가 없다.
더 편하게 쓰고 싶다면, 한 쪽이 다른 한 쪽을 서브 모듈 형태로 가지고 있으면 좋다.
아이디어 및 적용 방법
먼저, Spring Boot가 아니라 Spring의 경우 HTML 응답을 생성하는 것이 가능하다.
곰곰히 생각해보니 그 얘기는 리액트를 빌드한 index.html을 제공하는 것이 가능하다는 얘기였다.
그래서 Spring의 webapp 폴더내에 리액트 빌드 결과를 넣었다.
나의 Spring 프로젝트는 src/main/webapp/WEB-INF 내부에 html이나 jsp를 넣도록 되어 있었기 때문에
src/main/webapp/WEB-INF/fe 폴더 내부에 npm run build로 생성된 결과물을 넣었다.
그러고 나서 backend와 구분하기 위해서 domain.com/fe/** 경로를 사용하도록 설정하기로 했다.
그리고 기본적으로 domain.com/ 만 입력하면 backend가 아니라 frontend가 보이게 하기 위해서 WelcomeController에서 아래와 같이 기본 / 요청에 대한 응답을 정해줬다.
@Api(tags="Redirection용도")
@Controller
@CrossOrigin(origins="*")
public class WelcomeController {
@ApiOperation("Redirection 용도")
@GetMapping("/")
public String welcome() {
return "redirect:/fe";
}
}
@API나 @ApiOperation 태그들은 swagger-ui 용 태그이니 무시해도 된다.
그런데 그냥 /fe만 해서는 WEB-INF 내부의 자원에 접근할 수 없기 때문에 dispatcher-servlet.xml에서 url 매핑을 설정해줘야 한다.
이 url 요청은 동적으로 응답을 생성하는 게 아니라 정적 자원을 반환할 거라는 설정이다.
<!-- react용 url 강제 매핑 -->
<mvc:resources mapping="/fe/**" location="/WEB-INF/fe/" />
<mvc:view-controller path="/fe" view-name="forward:/fe/index.html" />
<mvc:view-controller path="/fe/login" view-name="forward:/fe/index.html" />
<mvc:view-controller path="/fe/sugang" view-name="forward:/fe/index.html" />
<mvc:view-controller path="/fe/board" view-name="forward:/fe/index.html" />
fe/**: fe 어쩌구 저쩌구 요청은 /WEB-InF/fe/ 경로 내부로 접근할 수 있게 해준다.
기본 경로가 fe로 설정되어 있으면 리액트에서 포함하고 있는 js들이 fe/** 이기 때문에 이 설정이 있어야지 페이지가 제대로 로드된다.
그리고 리액트 라우터에서 특별히 사용하고 있는 url의 경우 직접적으로 설정이 필요하다.
나 같은 경우 /login, sugang, 그리고 /board와 같은 url로 라우팅이 되도록 설정되어 있다.
이때, 실제로 fe나 fe/login, fe/sugang, fe/board 같이 리액트 라우터에서 사용하고 있는 커스텀 url 모두 결국 react에서는 index.html이 받아서 처리하는 것이기 때문에 강제로 forward를 걸어주는 것이다.
자 마지막으로 react에서도 설정을 해주자.
import { defineConfig, ConfigEnv, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import fs from 'fs';
export default defineConfig(({ mode }) => {
const isDev = mode === 'development';
return {
base: '/fe',
plugins: [react()],
server: {
...(isDev && {
https: {
key: fs.readFileSync('./mkcert/localhost+1-key.pem'),
cert: fs.readFileSync('./mkcert/localhost+1.pem'),
},
proxy: {
'/manage': {
target: 'https://trinity.dobby.kr',
changeOrigin: true,
secure: false,
rewrite: (path) => path, // 경로 그대로 전달
},
'/trinity': {
target: 'https://trinity.dobby.kr',
changeOrigin: true,
secure: false,
rewrite: (path) => path, // 경로 그대로 전달
}
}
})
}
}
});
나는 일단 react + vite + ts를 사용중이다.
vite.config.ts에서는 base 부분에 base로 사용될 url을 설정해준다.
아래에 있는 server의 프록시 부분은 dev일 때도 https로 서버랑 소통하려고 덧붙인 내용이다. 무시해도 된다!
추가로 App.tsx의 route에서도 설정을 해주자. (basename='/fe' 설정)
전부 /fe를 기본 url로 사용하기 위한 설정들이다.
<BrowserRouter basename='/fe'>
<div className='imgWrapper'><img src='/fe/logo.png'></img></div>
<Routes>
<Route path="/" element={<Login />} />
...
</Routes>
</BrowserRouter>
이렇게 리액트에서도 설정이 마무리 되었다.
자동화
매번 빌드 후 이 결과물을 spring 프로젝트 내부로 옮기는 명령어를 실행하는 것은 매우 귀찮은 일이다.
그래서 나는 쉘 스크립트를 작성해서 이 작업을 편하게 만들었다.
# 1. 의존성 추가
npm install
# 2. 빌드
npm run build
# 빌드 결과물 위치
BUILD_DIR="./dist"
# 타겟 경로
TARGET_DIR="../src/main/webapp/WEB-INF/fe"
# 3. 기존 폴더 삭제 후 복사
rm -rf "$TARGET_DIR"
mkdir -p "$TARGET_DIR"
# 4. 복사
cp -r "$BUILD_DIR/"* "$TARGET_DIR/"
echo "Build completed and files moved to $TARGET_DIR"
나 같은 경우 아래의 구조를 따르고 있다.
Spring Project
ㄴ Frontend
ㄴ node_modules
ㄴ src (Frontend sources)
ㄴ index.html
ㄴ build.sh
...
ㄴ src (Backend sources)
ㄴ main
...
위와 같은 위치에 스크립트를 작성해둔 상태이다.
그래서 타겟 경로가 상대 경로로 저렇게 된다.
이 스크립트가 실행 가능해야 하기 때문에 chmod +x 명령어로 실행할 수 있는 형태로 만들어준 후 ./build.sh 명령어를 통해 실행하면 빌드 및 빌드 결과 이동이 한 번에 완료된다!!
결론적으로 이제 frontend와 backend가 하나의 컨테이너에서 실행되며 이로 인해서 리버스 프록시가 걸리더라도 하나의 서브 도메인을 사용하기 때문에 CORS나 Same-Site를 신경쓸 필요가 없어졌다.
그래서 세션 쿠키 유지를 걱정하거나 CORS 처리를 Spring Security에서 따로 해줄 필요성이 사라졌다.
휴.. 이게 얼마나 편한지,, 진작 처리할 걸 그랬다..
심지어 아아아아~~주 예전에는 리액트 프로젝트를 실행하기 위해서 node 컨테이너를 만들고 거기서 npm run dev 명령어를 이용해서 실행했었다 ㅋㅋㅋㅋㅋ....
'개발' 카테고리의 다른 글
| [Spring] BE에서 로깅이 하고 싶다면? (4) | 2025.11.16 |
|---|---|
| [Gerrit, Jenkins] Gerrit과 Jenkins 연동 (1) | 2025.10.19 |
| Github Action으로 웹 앱 자동 배포하기 (with SSH 명령어) (0) | 2025.10.09 |
| Ubuntu에 도커 (Docker) 및 도커 컴포즈 (Docker-Compose) 설치 방법 (2) | 2025.09.03 |
| [WSL2] WSL2에 Window PC의 포트를 포워딩하는 방법 (6) | 2025.07.24 |