Search
Duplicate

JSP에서 React 사용하기

React 기반 프로젝트만을 다루다가, JSP + JQuery로된 프로젝트를 맡게되었을 때는 막막함과 불편함 그 자체였습니다. 특히 여러 사람의 손을 타며 일관성없이 성장해온 프로젝트인지라 제약사항도 많고, $('tag').append$('tag').on을 통해서 뷰와 액션을 추가해주다보니, 파일의 크기가 커지고 간혹 하나의 js 파일에 2만줄이 넘어가는 파일도 존재해 최악의 개발경험을 주었습니다.

JSP + React

새로운 Repository에 작업을 한다면 React를 사용하는데 문제가 없겠지만, 기존 프로젝트에서 사용하던 Layout의 코드들과 보안 관련 코드들이 너무나도 방대한데 프로젝트 자체에 대한 이해도가 높지 않은 상태에서 이를 진행하는데 시간적, 기술적 여유가 부족한 상태였습니다.
따라서 기존 프로젝트에서 제공하는 Layout등 코드들은 그대로 사용하되, 페이지 컨텐츠만 React로 작성하고자 했습니다.

기존 프로젝트의 빌드 방식

기존의 방식의 경우 glup을 통해 아래와 같이 빌드되었습니다.
const compile = () => { return src(path, { sourcemaps: true }) .pipe(babel()) // ES6를 ES5로 변경 : 환경설정은 .babelrc .pipe( // release/js에 기존 파일 경로와 동일하게 생성됩니다. dest(`${dest}/release/js`, { sourcemaps: '.', }), ); }; ... exports.default = series(..., compile, ...);
JavaScript
복사
glup.js
<main> <%@ include file="/WEB-INF/views/.../header.jsp"%> <%@ include file="/WEB-INF/views/.../resources.jsp" %> ... // webpack을 통해 bundling된 js <ver:script src="/resources/.../release/js/.../recruit.js"></ver:script> </main>
JavaScript
복사
index.jsp
JSP에선 .js 파일을 가져와 호출한다는 점에서 착안해, 번들링된 React 파일을 동일하게 가져온다면 JSP를 통해서 React를 사용할 수 있다는 생각이 들었습니다.

빌드된 index.js 파일 만들기

script 태그를 통해 가져올 index.js파일을 만드는 작업이 우선적으로 필요했습니다. 이를 위해서 webpack + babel을 통해서 React 파일을 glup에서 했던 경로와 동일하게 적용 후 번들링 해주었습니다.
const path = require('path'); exports.option = function() { return { mode: 'production', path: path.join(__dirname, './src/main/webapp', path), filename: 'index.js' module: { rules: [ { test: /\.(ts|js)x?$/i, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', ['@babel/preset-react', {"runtime": "automatic"}], '@babel/preset-typescript' ], plugins: [...] } } }, { test: /\.scss$/, use: [ { loader: 'style-loader' }, ... ], }, ... ] }, }; };
JavaScript
복사
webpack.config.js
이후 번들링 된 index.js를 jsp에서 호출해주어 JSP에서 React 코드를 적용하는데 성공했습니다.
<main> <%@ include file="/WEB-INF/views/.../header.jsp"%> <%@ include file="/WEB-INF/views/.../resources.jsp" %> ... // webpack을 통해 bundling된 js <ver:script src="/resources/.../release/js/.../index.js"></ver:script> </main>
HTML
복사
index.jsp

한계점

CRA를 통해 프로젝트를 생성해보신 분이라면, React의 코드를 수정했을 때 수정사항이 바로 브라우저에 적용되는 것을 알 수 있습니다. 이를 react-hot-loader와 같은 라이브러리들이 처리해주는데, JSP의 경우 가지고있는 지식으로는 구현이 불가능했습니다.
react파일을 webpack —watch 이 수정사항을 캐치해 번들링은 가능했지만, 해당 수정사항을 servlet이 intellij의 캐싱등으로 인해 브라우저에 수정사항이 적용되는데 오랜시간이 걸렸습니다.

Node Server를 통한 HMR 적용

Node Server를 통해 기존 프로젝트에 영향을 끼치지 않으면서 한계점을 극복하고자 했습니다.

HMR 적용

Vite를 통해 HMR을 적용 시켜주는 Node 서버를 작성했습니다. [Vite Custom SSR]
... const vite = await createServer({ server: { middlewareMode: 'ssr' }, appType: 'custom', }); app.use(vite.middlewares); app.use('*', async (req, res) => { try { const jspUrl = `http://${req.hostname}:8080`; // 외부 JSP 서버에서 HTML을 가져옴 const response = await axios.get(`${jspUrl}${req.originalUrl}`, {...req}); const entryClient = '<script src="/index.tsx" type="module"></script>'; const regex = /<!--entry-client-start-->\s*[\s\S]*?\s*<!--entry-client-end-->/; const transformHtml = await vite.transformIndexHtml(req.originalUrl, entryClient); // 특정 표시된 항목을 HMR로 변경 const html = response.data.replace(regex, transformHtml); res.status(200).set({ 'Content-Type': 'text/html' }).end(html); } catch (e) { vite?.ssrFixStacktrace(e); console.log(e.stack); res.status(500).end(e.stack); } });
JavaScript
복사
server.js
다음 호출 할 jsp에도 원하는 코드가 대치되도록, 대치항목을 표시해두었습니다.
<main> ... <!--entry-client-start--> // build된 파일 경로 <ver:script src="/resources/.../release/js/.../recruit.js"></ver:script> <!--entry-client-end--> </main>
JavaScript
복사
index.jsp
Node server의 지정포트로 브라우저를 열었을 때 처음 목적이였던 HMR는 정상적으로 동작했습니다.

Proxy 추가

하지만 기존 서비스의 경우 쿠키를 통해 로그인 유지여부를 판단하고 있고, 특정 API의 경우 token을 가지고 있어야해 개발 페이지까지 접근이 불편했습니다. 그리고 페이지 접근이 기존 JSP에서 <script>를 통해서 가져오던 JS와 CSS 파일들이 비정상적으로 동작하다보니 React 코드를 제외한 나머지 화면이 깨지는 현상이 있었습니다.
그래서 html파일을 기존 JSP 서버에서 불러오듯이, 타 코드들도 proxy middleware를 두어 경로를 변경해 가져오는 것을 고려했습니다.
// React 코드를 처리하기위해 호출하는 파일 확장자들은 proxy에서 제외해줍니다. app.use( createProxyMiddleware( [ '**', '!**/@vite/**/*', '!**/@react-refresh', '!**/*.tsx', '!**/*.ts', '!**/*.scss', '!**/*.mjs', ], { router: (req) => { return { protocol: 'http:', host: req.hostname, port: 8080, }; }, }, ), ); // vite middleware for HMR }
JavaScript
복사
server.js

최종 Node Server

React의 root가 되는곳이 여러곳에 산재해 있어 이를 하나의 파일에서 필요한 서버를 실행시켜 처리할 수 있도록 커스텀한 코드입니다.
const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const { createServer } = require('vite'); const { join } = require('path'); const prevServerPort = process.env.MRS_PORT || 8080; const port = process.env.PORT || 5173; const ROOT = { ROOT1: 'src/main/webapp/resources/.../root1', ROOT2: 'src/main/webapp/resources/.../root2', ROOT3: 'src/main/webapp/resources/.../root3', ROOT4: 'src/main/webapp/resources/.../root4', }; async function createDevServer(root) { const app = express(); const vite = await createServer({ configFile: join(__dirname, '../vite.config.ts'), server: { middlewareMode: true }, appType: 'custom', root: ROOT[process.env.VITE_ROOT], }); app.use(vite.middlewares); app.use( createProxyMiddleware( [ '**', '!**/@vite/*', '!**/@react-refresh', '!**/*.tsx', '!**/*.ts', '!**/*.scss', '!**/*.mjs', ], { changeOrigin: true, logger: console, selfHandleResponse: true, router: (req) => { return { protocol: 'http:', host: req.hostname, port: prevServerPort, }; }, onProxyReq: (req) => {}, onProxyRes: async (proxyRes, req, res) => { const bodyChunks = []; proxyRes.on('data', function (chunk) { bodyChunks.push(chunk); }); proxyRes.on('end', async () => { try { const body = Buffer.concat(bodyChunks); res.status(proxyRes.statusCode); Object.keys(proxyRes.headers).forEach((key) => { res.append(key, proxyRes.headers[key]); }); if ( proxyRes.headers['content-type'] && proxyRes.headers['content-type'].includes('text/html') ) { let html = body.toString(); const entryClient = '<script src="/index.tsx" type="module"></script>'; const regex = /<!--entry-client-start-->\s*[\s\S]*?\s*<!--entry-client-end-->/; const transformHtml = await vite.transformIndexHtml( req.originalUrl, entryClient, ); html = html.replace(regex, transformHtml); res.send(new Buffer.from(html)); } else { res.send(body); } } catch (e) { vite?.ssrFixStacktrace(e); console.log(e.stack); res.status(500).end(e.stack); } }); }, }, ), ); return { app }; } createDevServer().then(({ app }) => app.listen(port, (name) => { console.log(`Server Start : http://localhost:${port}`); }), );
JavaScript
복사
server.js

Vite로 빌드하기

webpack + babel로 되어있는 프로젝트의 빌드속도를 향상시키기 위해 처리를 시작했습니다.
각 프로젝트별로 약 30초의 시간이 걸려 프로젝트 5개를 빌드하는데 총 150초란 긴 시간이 걸렸습니다.

기존 구조

파일 구조

A
A.jsp
index.tsx
B
B.jsp
index.tsx
C
C.jsp
index.tsx

A.jsp, B.jsp, C.jsp

... <div id="root"></div> <!--entry-client-start--> <script src="/resources/A/index.js"></script> <!--entry-client-end--> ...
HTML
복사

A/index.tsx

import * as ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import Portals from '@common/components/Portals/Portals'; import A from '@A'; ReactDOM.render( <BrowserRouter> <A /> <Portals /> </BrowserRouter>, document.getElementById('root'), );
JavaScript
복사
webpack, babel이 비교적 느림 + 5개의 파일이 개별적으로 빌드됨으로 인해서 빌드 속도가 150초라는 상당히 긴 시간이 걸렸습니다.

1차 : 5개를 1개로 만들기

우선적으로 5개가 빌드되던 환경을 1개로 줄여줬습니다. 이를 위해서 A, B, C에 각각 다른 js를 호출하던 로직을 하나의 js를 호출하도록 변경해주었습니다.

A.jsp, B.jsp, C.jsp

... <div id="screening-root"></div> <!--entry-client-start--> <ver:script src="/resources/mrs2/release/js/index.js"/> <ver:link rel='stylesheet' href='/resources/release/A/assets/index.css' /> <!--entry-client-end--> ...
HTML
복사
각 JSP에 하나로 빌드된 index.js와 index.css를 import 해주도록 변경해줍니다.

index.tsx

// https://ko.vitejs.dev/guide/backend-integration#backend-integration import 'vite/modulepreload-polyfill'; import './A/index.tsx'; import './B/index.tsx'; import './C/index';
JavaScript
복사
root/index.tsx에 각 프로젝트의 루트들을 호출하도록 수정해줍니다.

A/index.tsx

import * as ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import Portals from '@common/components/Portals/Portals'; import Screening from '@A'; const aRoot = document.getElementById('A-root'); if (aRoot) { ReactDOM.render( <BrowserRouter> <A /> <Portals /> </BrowserRouter>, aRoot, ); }
JavaScript
복사

vite로 모든 프로젝트 동시에 빌드

import react from '@vitejs/plugin-react'; import tsconfigPaths from 'vite-tsconfig-paths'; import type { UserConfig } from 'vite'; const config: UserConfig = { logLevel: 'info', plugins: [ react({ babel: { parserOpts: { // mobx로 인해 decorators-legacy 사용 plugins: ['decorators-legacy', 'classProperties'], }, }, }), tsconfigPaths(), ], build: { rollupOptions: { input: './src/main/webapp/resources/A/index.tsx', output: { dir: './src/main/webapp/resources/A/release/react', entryFileNames: '[name].js', }, }, }, }; export default config;
JavaScript
복사

2차 : vite로 빌드하기 위해 처리

A.scss로 되어있던 파일을 A.module.scss로 변경

vite의 경우 scss파일에 .module.scss를 붙여주기를 권고하기 때문에 처리해주었습니다.

vite.config.ts 파일 수정

import react from '@vitejs/plugin-react'; import tsconfigPaths from 'vite-tsconfig-paths'; import type { UserConfig } from 'vite'; import postcss from 'rollup-plugin-postcss'; import url from 'postcss-url'; const config: UserConfig = { logLevel: 'info', plugins: [ react({ babel: { parserOpts: { plugins: ['decorators-legacy', 'classProperties'], }, }, }), tsconfigPaths(), ], build: { rollupOptions: { input: './src/main/webapp/resources/A/index.tsx', output: { dir: './src/main/webapp/resources/A/release/react', entryFileNames: '[name].js', assetFileNames: 'assets/[name][extname]', plugins: [ postcss({ plugins: [ url({ url: (asset) => { // asset: 처리 중인 객체, asset.url은 원본 URL입니다. // 여기서 '/your/specific/path/'를 URL 앞에 추가합니다. return `/resource/A/react/${asset.url}`; }, }), ], }), ], }, }, }, }; export default config;
JavaScript
복사
postcss를 처리를 통해서 css.background-url과 등의 url들을 변경해줬습니다. React가 아닌 jsp 내에서 사용하는 파일이기 때문에 이를통한 URL들의 변경이 필요했습니다.

풀어야 할 문제점

inline css등 다른 방법을 통해서 사용하는 url 처리 필요

이미지에 대한 경로에서 css의 background url과 파일 경로를 통해 이미지를 불러오는데는 문제가 없지만, inline css에 background url을 지정해주었을 때 이를 처리해줄 수 있는 방법을 찾아야합니다.

하나의 파일로 묶여있는 index.js 분리 필요

lazy를 통해서 import 선언 시 파일이 여러 번들로 분리되게 됩니다. 그리고 이러한 정보들은 빌드파일에 manifest.json에 명시되어 있습니다. index.js를 manifest.json을 JSP 파일에서 불러오기와 사용하도록 변경해줘야합니다.