예전에 방학을 맞아서 잠깐 간단한 todolist 사이드 프로젝트를 했었다. 디자인은 거의 velopert 님의 todolist를 따오듯이 하고, 몇가지 기능 정도 추가해서 만든 조잡한 프로젝트였다. (방학이 다 지나는 바람에 AWS에 빌드도 못했다.) 프론트엔드는 React로, 백엔드는 SpringBoot로 진행을 했었는데, 이번에 소스코드를 정리하다가 이 당시에 한번에 프론트엔드와 백엔드를 모두 빌드했던 것이 생각나서 그 방법을 까먹기 전에 정리하고자 이 글을 쓴다.
일반적으로 팀 단위 프로젝트로 빌드를 한다면 프론트엔드와 백엔드를 분리해서 서버를 따로 올린 뒤 따로 따로 빌드를 하겠지만, 이 당시에 나는 혼자서 사이드 프로젝트를 진행하는 만큼, 서버를 따로 분리해서 제각기 빌드를 한 뒤 따로따로 실행해서 http 통신으로 데이터를 주고받는 일을 하기 보다는 전체 SpringBoot 프로젝트 구조 안에 view 부분만 React로 만들어서 내부로 집어넣어 주는 것이 좋겠다고 생각했다.
Maven이 아닌 Gradle을 사용했고, Java 버전은 11, 프로젝트를 만들던 당시에는 Node.js 12 버전을 사용했던 것 같은데 어쨌든 현재 환경 기준으로 Node.js v14.18.1, npm v6.14.15를 사용했다.
SpringBoot 프로젝트를 생성한다. 나는 Spring Initializr Gradle을 기준으로 설명할 것이므로, Maven이 아닌 Gradle을 고르는 것을 잊지 말자.
SpringBoot 프로젝트 안에 프론트엔드 파일, 즉 React 프로젝트가 들어갈 디렉토리가 있어야 한다. 프로젝트 최상단에 front-end 라는 폴더를 만들어주었다. 백엔드 코드는 다른 프로젝트들과 마찬가지로 프로젝트 폴더 내에서 작성하면 되고, 프론트엔드 코드는 front-end 폴더 아래에서 기존 React 프로젝트를 작성하는 것 처럼 작성해주면 된다.
front-end 디렉토리로 이동해서 React를 설치한다. 터미널을 열어서 다음과 같이 작성한다.
npx create-react-app 프로젝트명
참고로 React 프로젝트 명은 대문자를 사용하면 안되기 때문에, 폴더 이름에 맞춰서 프로젝트 명을 "front-end"라고 지어주었다.
그 뒤 React로 프론트엔드 코드를 작성해주면 된다.
현재 상태로는 프로젝트 디렉토리는 통합되어 있지만, SpringBoot를 빌드하면 백엔드 서버만 빌드되고 프론트엔드는 따로 npm을 통해 빌드해야 한다. 이를 하나로 합쳐보자.
build.gradle에 다음과 같이 작성한다.
// npm install
task appNpmInstall(type: NpmTask) {
workingDir = file("${project.projectDir}/front-end")
args = ["install"]
}
// npm build
task npmBuild(type: NpmTask) {
workingDir = file("${project.projectDir}/front-end")
args = ["run", "build"]
}
// 빌드된 결과 이동
task copyWebApp(type: Copy) {
from "front-end/build"
into 'build/resources/main/static/'
}
npmBuild.dependsOn appNpmInstall
copyWebApp.dependsOn npmBuild
compileJava.dependsOn copyWebApp
기본적으로 프론트엔드 빌드는 npm install(최초 설치 이후에는 생략 가능) -> npm run build 의 과정으로 이어지는데, 위 코드는 해당 과정을 SpringBoot 빌드 시에 함께 실행하도록 해준다.
appNpmInstall task에서는 npm install을, npmBuild task에서는 npm run build를 실행한다. 이렇게 한다고 해서 SpringBoot의 view로 React가 나오지는 않는데, 빌드된 결과를 SpringBoot의 빌드 디렉토리로 옮겨주어야 하기 때문이다. copyWebApp task를 실행하면 빌드된 파일들을 build/resources/main/static 디렉토리로 옮겨서 SpringBoot 프로젝트에서 사용할 수 있도록 해준다.
맨 아래에
npmBuild.dependsOn appNpmInstall
copyWebApp.dependsOn npmBuild
compileJava.dependsOn copyWebApp
가 있기 때문에, appNpmInstall -> npmBuild -> copyWebApp 순으로 빌드 과정이 실행된다.
문제는 이렇게 빌드를 완료하더라도 프론트엔드 코드가 정상적으로 작동하지 않는다는 점이다. 정확히는 딱 메인 페이지만 작동하고 그 외의 페이지는 작동하지 않는다. React에서 Router를 이용하여 페이지 이동을 설정해 주어도 빌드한 웹 상에서 페이지를 이동하면 404 에러가 발생한다. 이는 실제 빌드하고 있는 Spring에서 해당 URL에 대한 Mapping 처리가 되어있지 않기 때문이다. 이는 URL에 대한 추가적인 처리를 해주는 것으로 해결할 수 있다.
@Controller
public class WebController implements ErrorController {
@GetMapping("/error")
public String handleError() {
return "/index.html";
}
@Override
public String getErrorPath() {
return "/error";
}
}
WebController는 에러가 발생하면 index.html로 이동하도록 유도한다. 이제 다시 빌드를 실행하면 정상적으로 React와 Spring Boot가 합쳐진 웹 어플리케이션이 작동한다.