몇달전까지 Spring Framework로 이루어진 오래된 웹 프로젝트를 담당하고 있었습니다. 오래된 레거시라 페이지는 jsp 파일로 되어있고 그 위에 프론트 코드가 jQuery로 짜여져 있는 형태였습니다.
아무래도 DOM에 직접 접근하는 jQuery의 특성 때문에 유지보수가 상당히 까다로웠습니다. 여기에 개인적으로 불만도 되게 많았죠. 그래서 리액트를 도입하기로 했습니다!!
SpringBoot + React 연동해서 프로젝트를 생성하는 글은 많이 찾아볼 수 있었지만 jsp를 사용하는 레거시 Spring framework 위에 React를 얹는 내용은 없는 것 같아 글로 남겨보려 합니다.
가장 처음으로 시도한 방법은 빈(empty) jsp 위에 리액트 코드를 올리는 것이었습니다.
일단 일반적인 리액트 앱과 우리의 큰 차이점 중 하나는 Single Page Application의 여부입니다.
Single Page가 아니란 것은 Page를 그리는 파일이 여러개라는 뜻이겠죠! 이때는 라우팅을 서버의 Controller를 통해 하게 되는데 리액트에게 위임하기 위해 아래와 같은 구조로 변경이 필요했어요.
그리고 jsp 파일은 아래와 같은 모양이 되었죠.
...
<div id="root"></div>
<script src="${SRC_PATH}/index.js"></script>
...
리액트에서 볼 수 있는 그 index.html과 유사하네요.
이런 방식으로 하게되면 Single Page 역할을 하게 되는 index.jsp가 여러개(말이 좀 이상하지만)가 만들어집니다. 프로젝트 구조 상 하나의 jsp 파일만 만들 순 없었습니다..ㅠ
아무튼 그래서 webpack.config는 아래와 같은 식으로 작성했습니다.
const opt = (outputPath: OutputPath) => {
return {
mode: 'production',
// ...공통적인 webpack 설정...
};
};
const screening = Object.assign({}, opt(OutputPath.SCREENING), {
name: 'screening',
entry: `${path}/screening/index.tsx`,
output: {
path: __dirname + '/src/main/webapp' + OutputPath.SCREENING,
filename: 'index.js',
},
});
/** screening과 같은 식으로 여러개의 객체를 만듭니다.
...
...
...
*/
module.exports = [screening/**, ...다른 객체들*/];
일반적인 webpack.config와 다른 것은 index.jsp의 갯수만큼의 설정을 만들어주어야 했다는 것이었어요. jsp를 하나만 둘 수는 없는 상황이었기 때문에 여기까지는 이게 최선이었죠. jsp 파일이 여러개이기 때문에 entry도 여러개 있고 각 entry마다 index.tsx가 있습니다.
좀 이상한 모양이긴 하지만 어쨌든 드디어 프로젝트 안에서 리액트 코드를 사용할 수 있었습니다.
그러나 문제가 있었어요...
jsp 위에 리액트 코드를 얹어놓은 형태라 hot-reload를 사용할 수 없었습니다. 개발할 때 꽤나 불편하더군요. 백엔드와 프론트엔드가 분리되어있지 않다보니 이런 문제도 있네요!!ㅠㅠ
그럼 hot-reload를 사용하기 위해 프론트엔드 앱을 따로 구축하여 실행시키도록 하겠습니다.
프론트와 백엔드를 분리하기 위해서 첫번째로 React 프로젝트를 따로 생성해주어야 했습니다. 'SpringBoot + React 연동'이라고 검색하면 찾아 볼 수 있는 일반적인 구조로 React 프로젝트를 생성했어요.
📁 프로젝트 폴더
ㄴ 📁 src (기존 서버 폴더)
ㄴ 📁 frontend (새로 생성한 리액트 폴더)
이 새로 만든 프로젝트 폴더 내에서는 일반적인 React 프로젝트 개발하듯이 작업하면 된답니다.
지금부터는 이 두개의 폴더로 구분된 프로젝트를 구분해서 설명하기 위해 기존 서버 폴더는 "스프링 프로젝트", 새로 생성한 리액트 폴더는 "리액트 프로젝트"라고 부르겠습니다.
그럼 새로 분리해서 작업한 리액트 프로젝트의 페이지를 화면에 출력해야 합니다.
이전에 처리했던 방식은 스프링 프로젝트에서 모든 화면을 관리하고 있는 형태였습니다. 이제 리액트 프로젝트에서 리액트로 개발된 화면을 관리하게 되었고, 이 화면을 띄워줄 수 있어야 합니다.
우선 개발 및 운영 환경을 고려하여 webpack 설정을 수정해줄 필요가 있습니다.
개발 시에는 devServer를 사용할 것이기 때문에(프론트 서버를 분리하게 된 근본적 이유이죠) webpack.config.js의 development.js 아래 설정을 추가해줬습니다.
webpack.config.js > development.js
devServer: {
port: devServerPort,
allowedHosts: [`${host}`],
historyApiFallback: true,
host: '0.0.0.0',
proxy: {
'**': `http://localhost:${process.env.SERVER_PORT}`,
},
},
그리고 배포 시 리액트 프로젝트에서 빌드된 추출물이 서버 프로젝트에 포함되어야 하기 때문에 production.js에서 output 경로를 서버 프로젝트 내부로 설정했습니다.
webpack.config.js > production.js
output: {
publicPath: './react',
path: `${project}/src/main/webapp/react`
},
이렇게 설정하면 src > main > webapp > react 폴더에 빌드된 파일이 생성됩니다.
스프링 프로젝트에서 루트('/') 요청에 대해 index.jsp 페이지를 응답하도록 mapping되어 있었습니다. 이 index.jsp를 살짝 수정하여 루트('/') 요청 시 리액트 페이지를 응답받도록 수정해보겠습니다.
src > main > webapp > index.jsp
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>
// 리액트 빌드 파일 포함하기
<%@include file="./react/index.html"%>
리액트 프로젝트를 빌드하여 나온 파일인 src > main > webapp > react > index.html 파일을 포함하도록 합니다.
다음으로는 resource mapping을 해주어야 합니다. src > main > webapp > react 라는 위치는 제가 임의로 만들어서 사용하는 경로이기 때문에 해당 경로에 있는 파일들을 resource로 사용하기 위한 작업을 해주는 것이죠.
web.xml 파일을 열어보면 다른 xml 문서들이 mapping되어 있는 것을 확인할 수 있어요. (예: servlet-context.xml 등)
그 중 필자의 경우 resource-context.xml 파일에 여러 resource들이 mapping되어 있었습니다. (파일 이름은 다를 수 있어요.)
해당 파일에 react라는 경로를 mapping시켜주면 됩니다.
<resources mapping="/react/**" location="/react/"/>
이제 스프링 프로젝트에서 구동하는 앱에 리액트 프로젝트에서 만든 페이지를 띄워주는 일이 남았네요.
이전에 리액트 + 스프링부트를 사용하여 작업할 때 페이지 새로고침 시 아래와 같은 에러 화면을 발견한 적이 있었습니다.
이때 에러 페이지 컨트롤러에서 페이지 요청에 대한 응답을 index.html로 주어 해결했었습니다. 이 접근 방식으로 리액트 프로젝트에서 관리하는 페이지를 띄우도록 할게요.
아래 그림을 보면 기존에 구현되어 있는 페이지 (/page1
, /page2
, /page3
)는 Controller에 mapping이 되어있습니다.
그리고 여기에 /page4
라는 페이지를 리액트 프로젝트에서 추가할 때 /page1
, /page2
, /page3
와는 다르게 Controller에 새로 mapping하지 않습니다.
그러면 스프링 프레임워크 내부적으로 404 에러를 발생시키게 됩니다. 기존 프로젝트에서 404 에러가 발생하면 404.jsp 파일을 응답으로 보내주고 있었습니다.
저는 이 404 페이지에서 리액트 페이지로 리다이렉트 되도록 아래 코드를 추가해주었습니다.
<script>
sessionStorage.setItem('React pathname', window.location.pathname);
window.location.href = '/';
</script>
리다이렉트 전에 sessionStorage에 window.location.pathname을 넣어주었습니다. 이는 리액트 앱에서 적절하게 라우팅할 수 있도록 하기 위함이에요.
물론 리액트 프로젝트에서 이 값을 사용 후에 지우는 로직도 구현해 주셔야 합니다!
루트 페이지로 리다이렉트 시키면 위에서 index.jsp를 수정할 때 index.jsp가 리액트 프로젝트에서 빌드된 파일을 포함하도록 했기 때문에 리액트 페이지로 잘 이동하게 됩니다!!!
프론트엔드 서버(리액트 프로젝트)를 분리하는데 성공했지만 실제 운영 환경에 배포된 있는 형상은 jsp 위에 React(코드) 올리기입니다.
리액트 프로젝트를 분리하면서 모든 페이지에 공통으로 들어가는 레이아웃 영역(header, SNB 등)을 다시 개발해야 했고, 실제로 개발까지는 했으나 버그가 있는 채로 production 환경에 배포되면 안되기 때문에 아주 꼼꼼한 검증이 필요했습니다. 그러나 시간 및 인력 부족의 문제(다른 개발해야 할 항목이 많았기에..)로 실제 운영에 배포까지 하지는 못했습니다.ㅠㅠ
아무튼 이 작업을 통해 새로 개발하는 페이지는 리액트로 개발할 수 있게 되었네요.
그리고 확실히 유지보수 측면에서 JQuery로 작성된 코드보다 React로 작성된 코드가 훨씬 낫다는 점을 체감할 수 있었습니다.