해당 포스트에서 사용해 볼 프로젝트 구조는 다음과 같습니다.
일반적인 WebMVC
프로젝트에서 자바스크립트를 사용하여 웹 페이지를 구성할 때 번거로운 부분은 대부분의 브라우저에서 자바스크립트 실행을 보장하기 위해 개발자가 직접 ES5
코드를 작성해야 한다는 점입니다.
물론 미리 컴파일된 babel/standalone 라이브러리를 사용하여 text/babel
타입의 스크립트를 사용하면 당장에 동작에는 문제가 없으나, 실제로 권장하는 방법은 아닙니다.
해당 라이브러리를 사용하면 스크립트를 작성하면,
<script type="text/babel" data-presets="env,stage-3">
const someFunc = async () => {
const { myData } = (await axios('..')).data;
return myData;
}
</script>
런타임에 babel
이 해당 구문을 해석하여 ES5
스크립트로 변환해서 실행시켜주기는 하나, 바벨의 폴리필이 전역 오염을 일으키는 등의 부작용이 있습니다.
따라서 ES6
이상의 구문을 사용하기 위해서는 별도의 프론트엔드 스크립트를 사전에 빌드하여 포함할 수 있도록 노드 프로젝트를 구성할 필요가 있습니다.
스프링 프로젝트 내부의 src/main/resources/frontend
디렉터리에서 다음 node
프로젝트를 구성합니다.
$ npm init -y
$ npm install --save-dev webpack webpack-cli
$ npm install --save-dev @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader
$ npm install --save @babel/runtime @babel/runtime-corejs3
ES6+
구문을 ES5
로 컴파일하기 위한 바벨 설정은 다음과 같습니다.
// .babelrc.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: [
'> 0.25%',
'not dead',
'ie >= 9',
],
}],
],
plugins: [
['@babel/plugin-transform-runtime', {
absoluteRuntime: false,
corejs: 3,
helpers: true,
regenerator: true,
}],
],
};
webpack
설정은 다음과 같습니다.
// webpack.config.js
const path = require('path');
const EnvironmentPlugin = require('webpack/lib/EnvironmentPlugin');
const prefix = './src/page';
const output = '../static/js';
module.exports = ({ NODE_ENV }) => ({
mode: NODE_ENV,
target: ['web', 'es5'],
entry: {
'index': `${prefix}/index.js`,
},
output: {
path: path.resolve(__dirname, output),
filename: '[name].js',
},
resolve: {
alias: {
'~': path.resolve(__dirname, 'src'),
},
extensions: ['.js'],
},
module: {
rules: [
{
test: /\.js$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'babel-loader',
},
],
},
plugins: [
new EnvironmentPlugin({
NODE_ENV,
}),
],
devtool: false,
});
package.json
에는 취향껏 빌드 스크립트를 추가합니다.
## package.json
{
..
"scripts": {
"build": "webpack --env NODE_ENV=development",
"build:prod": "webpack --env NODE_ENV=production"
},
..
}
테스트를 위해 src/main/resources/frontend/src/index.js
위치에 다음 스크립트를 작성합니다.
// index.js
const asyncFunc = () => new Promise((resolve) => {
setTimeout(() => resolve(), 500);
});
(async () => {
console.debug('task started..');
await asyncFunc();
console.debug('task finished.');
})();
정상적으로 빌드되는지 확인합니다.
$ npm run build
---
..
modules by path ./node_modules/core-js-pure/ 164 KiB 297 modules
modules by path ./node_modules/@babel/runtime-corejs3/ 18.6 KiB
modules by path ./node_modules/@babel/runtime-corejs3/core-js-stable/ 1.04 KiB 16 modules
modules by path ./node_modules/@babel/runtime-corejs3/core-js/ 736 bytes 11 modules
modules by path ./node_modules/@babel/runtime-corejs3/helpers/ 16.4 KiB 4 modules
./node_modules/@babel/runtime-corejs3/regenerator/index.js 448 bytes [built] [code generated]
modules by path ./src/ 201 KiB
...
webpack 5.74.0 compiled successfully in 1350 ms
ES5
에서 지원하지 않는 폴리필이 주입되는 것을 확인할 수 있습니다.
스프링 프로젝트가 빌드될 때, 자동으로 위 노드 프로젝트를 빌드할 수 있도록 플러그인이 존재합니다.
해당 플러그인을 스프링부트 프로젝트의 build.gradle.kts
의 플러그인 클로저에 추가합니다.
// build.gradle.kts
plugins {
..
id("com.github.node-gradle.node") version "3.4.0"
}
설정부분과 task
를 추가하면 코틀린 코드 빌드시 프론트엔드 프로젝트를 먼저 빌드할 수 있습니다.
// build.gradle.kts
import com.github.gradle.node.npm.task.NpmTask
..
node {
// 프론트엔트 프로젝트의 node, npm 버전과 같게 합니다.
val frontendNodeVersion = "16.17.0"
val frontendNpmVersion = "8.15.0"
val userHome = System.getProperty("user.home")
// node 바이너리가 저장될 위치를 지정합니다.
val nodeDir = file("${userHome}/.node/$frontendNodeVersion")
// npm 바이너리 저장될 위치를 지정합니다.
val npmDir = file("${userHome}/.npm/$frontendNpmVersion")
// 빌드할 프로젝트의 위치를 지정합니다.
val projectDir = file("${project.projectDir}/src/main/resources/frontend")
version.set(frontendNodeVersion)
npmVersion.set(frontendNpmVersion)
download.set(true)
workDir.set(nodeDir)
npmWorkDir.set(npmDir)
nodeProjectDir.set(projectDir)
}
..
// 'npm run build:prod' 를 실행하는 task를 추가합니다.
val frontendTask = tasks.register<NpmTask>("frontendTask") {
args.set(listOf("run", "build:prod"))
}
tasks.withType<KotlinCompile> {
// 코틀린 빌드 작업은 frontend 빌드 작업이 선행되어야 합니다.
dependsOn(frontendTask)
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = java.sourceCompatibility.toString()
}
}
이렇게 구성하면 ES6+
구문의 자바스크립트를 자유롭게 사용할 수 있습니다.
node_modules
가 깃에 업로드되지 않게 하기 위해 .gitignore
파일에 node_modules
디렉터리를 추가합니다.