Skip to main content

VanillaJS로 SPA 환경 구축하기 - Step3: Webpack5

· 12 min read

개발 코드가 변경될 때마다 반영을 위해 서버를 재시작해주는 것은 매우 비효율적이고 귀찮은 일인데, 이러한 부분 예방(?)을 위해서... nodemon을 이용하여 적용할 수도 있지만, 다음과 같은 이유로 webpack을 사용하여 개발 환경을 재구성(?) 하였다.

  • 다앙햔 브라우저 지원
  • 번들링을 좀 더 원활하게 하기 위해
  • HMR
  • webpack5를 사용해보기 위해

Configure Webpack

먼저 가장 기본적인 webpack을 이용해서 다음과 같은 작업을 수행하도록 구현해보려고 한다.

  • typescript compile
  • express와 webpack 연동

Installation

webpack 관련 패키지들을 설치한다.

yarn add -D webpack webpack-cli

그리고 typescript를 이용하여 작성이 되어있으므로 babel-loader를 이용하여 트랜스파일을 하도록 설정한다.

  • babel-loader : 트랜스파일링 하기위한 파일 loader
  • @babel/core : babel config 파일을 읽어 해당 파일을 기반으로 트랜스파일링을 수행하기 위한 모듈
  • @babel/preset-env : 트랜스파일링 기본 preset 모듈
  • @babel/preset-typescript : 타입스크립트를 트랜스파일링 하기 위한 모듈
  • core-js@3 : polyfill 설정.
    • 런타임에서 실행된다.
    • 기존의 @babel/polyfil은 deprecated 되었음
yarn add -D babel-loader @babel/core @babel/preset-env @babel/preset-typescript
yarn add core-js@3

babel.config.js

module.exports = (api) => {
api.cache(true);

return {
presets: [
[
'@babel/preset-env',
{
targets: '> 0.5%, not dead',
useBuiltIns: 'usage' /* 필요한 모듈만 가져오도록 설정 */,
corejs: {
/* configuration about polyfill */
version: 3,
proposals: true,
},
},
],
'@babel/preset-typescript',
],
plugins: [],
};
};

Modify tsconfig.js

babel을 이용해서는 트랜스파일링을 하고, tsc를 이용하여 타입을 검사하기 위해 기존 작성한 tsconfig.js에 아래와 같은 설정을 적용해준다.

// tsconfig.js
"compilerOptions": {
//...SKIP

// tsc를 사용해 .js 파일이 아닌 .d.ts 파일이 생성되었는지 확인합니다.
"declaration": true,
"emitDeclarationOnly": true,
// Babel이 TypeScript 프로젝트의 파일을 안전하게 트랜스파일할 수 있는지 확인합니다.
"isolatedModules": true

//...SKIP
}

webpack.config.js

webpack.config.js 파일을 생성하고 아래와 같이 정의한다.

const webpack = require('webpack');
const path = require('path');

module.exports = {
mode: 'development',
target: 'web',
devtool: 'eval-cheap-source-map',
entry: {
app: path.resolve(__dirname, './src/frontend/static/js/index.ts'),
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
publicPath: '/',
// clear output directory before exporting bundle files
// don't need 'clean-webpack-plugin' anymore
clean: true,
},
module: {
rules: [
{
test: /\.[tj]sx?$/,
exclude: /node_modules/,
use: 'babel-loader',
},
],
},
plugins: [],
optimization: {},
resolve: {
modules: ['node_modules'],
extensions: ['.js', 'ts', '.json', '.jsx', '.tsx', '.css'],
},
};

Build Test

webpack 실행을 위한 script를 추가해주고, 바벨을 이용하여 하위 브라우저도 호환이 가능하도록 트랜스파일링된 번들 파일을 이용할 것이므로 package.json에 추가해주었던 `"type": "module" 설정을 삭제해준다.

{
//...SKIP
"scripts": {
"build": "webpck"
}
//...SKIP
}

yarn build를 수행하여 정상적으로 빌드가 되는 지 확인해본다.

yarn build
(node:2041) ExperimentalWarning: Custom ESM Loaders is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
asset app.bundle.js 1.71 KiB [compared for emit] (name: app)
./src/frontend/static/js/index.ts 28 bytes [built] [code generated]
webpack 5.72.1 compiled successfully in 390 ms

빌드가 정상적으로 수행이 되면 output에 정의한 경로에 번들링된 빌드 산출물이 생성된다.

ls -al dist
total 8
drwxr-xr-x 3 user staff 96 5 11 22:33 .
drwxr-xr-x 23 user staff 736 5 11 22:34 ..
-rw-r--r-- 1 user staff 1751 5 11 22:33 app.bundle.js

빌드된 산출물을 이용하여 웹화면이 정상적으로 호출이 되는 지 확인하기 위해 src/frontend/index.html을 열어서 script 파일의 경로를 수정한다.

<!-- index.html -->
<script type="module" src="../../dist/app.bundle.js"></script>

그리고 console 창을 열지 않고도 index.ts에 작성된 스크립트가 정상 동작하는 지 확인해보기 위해 아래와 같이 작성하였다.

// index.ts
document.querySelector('#app').innerHTML = '<h1>Welcome!! Hello! World!!!</h1>';

다시 빌드를 한 뒤에 IDE에서 src/frontend/index.html 파일을 Live Server 기능을 이용하여 실행하여 확인해본다. 정상적으로 빌드가 되고 실행이 되었다면 아래와 같은 화면을 확인해볼 수 있다. &#39;vanillajs-spa-webpack-init&#39;

webpack을 이용하여 번들링 되도록 구성하였으니, 폴더 구조를 수정하였다.

Add CSS Loader

index.html에 css 파일을 직접 정의하지 않고, js 스크립트 파일에서 직접 css 파일을 import하여 사용할 수 있도록 하기 위해 webpack에 설정을 추가하였다.

Installation

  • css-loader : js 파일에서 css 파일을 읽어온다.
  • style-loader : css-loader를 이용해 웹팩 의존성 트리에 추가된 string 값들을 DOM에 로 넣어준다.

css-loader로 읽어온 css 파일을 주입하기 위한 방법으로는 style-loader 또는 mini-css-extract-plugin를 사용할 수 있는데, 각 플러그인에 대한 동작 차이는 다음과 같은 글들을 참고하면 좋을 것 같다.

# webpack
yarn add -D css-loader style-loader

webpack.config.js

아래와 같이 css 모듈에 대한 설정을 추가해준다.

const webpack = require('webpack');
const path = require('path');

module.exports = {
//...SKIP
module: {
//...SKIP
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
//...SKIP
};

Modify Code

기존 작성된 코드에서 다음과 같은 부분들을 수정해준다.

  • index.html에서 다음의 코드를 제거한다
<link rel="stylesheet" href="/static/css/index.css" />
  • index.css 파일을 src/styles 폴더를 생성 후 해당 경로로 이동해주었다.
  • index.jssrc/frontend 하위로 이동하고 css 파일 주입을 위한 구문을 추가해준다.
import './styles/index.css';

지금까지 수정된 프로젝트의 구조는 아래와 같다.

src/frontend
├── index.html
├── index.ts
└── styles
└── index.css

Build Test

build를 수행해서 Live Server를 통하여 확인해보면 css 설정이 적용된 것을 확인할 수 있다.

번들링된 entry 파일을 동적으로 삽입하기

빌드된 결과를 자동으로 index.html 파일에 주입해주도록 설정한다.

  • html-webpack-plugin - entry를 html에 동적 삽입과 html 결과물을 내보내기 위한 플러그인
yarn add -D html-webpack-plugin

Template html

기존에 작성된 index.html 파일을 <project_root_dir>/public 폴더를 생성하여 해당 폴더로 이동 후, 아래와 같이 수정했다.

<!-- ./public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Single Page App (Vanilla JS)</title>
</head>
<body>
<div id="app"></div>
<!-- will be injected -->
</body>
</html>

webpack.config.js

html-webpack-plugin 설정을 추가해준다.

// webpack.config.js
//...SKIP
const HtmlWebpackPlugin = require("html-webpack-plugin");

//...SKIP
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "public/index.html"), // 읽어올 템플릿 경로 지정
publicPath: "",
}),
],
//...SKIP

webpack dev server

express를 이용하여 제공하던 서버를 webpack dev server를 이용하도록 설정한다. webpack dev server를 이용하게 되면 수정된 코드가 바로 컴파일 되어 반영될 수 있도록 하여 좀 더 원활한 개발 환경 설정이 가능하다.

Installation

webpack-dev-server 패키지를 설치한다.

yarn add -D webpack-dev-server

webpack.config.js

webpack 설정 파일에 devServer 설정을 추가해준다.

// webpack.config.js

//...SKIP
devServer: {
static: {
directory: path.join(__dirname, "public"),
},
compress: true,
open: true,
hot: true,
port: 8000,
},
};

package.json에 아래와 같이 스크립트를 추가해준다.

"dev": "webpack serve",

아래와 같이 추가한 스크립트를 실행하면 지정한 port를 이용하여 개발 서버가 기동되고, 웹브라우저를 통하여 확인할 수 있다.

yarn dev                                                              22:22:08
(node:90683) ExperimentalWarning: Custom ESM Loaders is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8000/
<i> [webpack-dev-server] On Your Network (IPv4): http://172.30.1.57:8000/
<i> [webpack-dev-server] On Your Network (IPv6): http://[fe80::1]:8000/
<i> [webpack-dev-server] Content not from webpack is served from '/Users/gloria/Documents/mio/cocoa-clone/public' directory
asset app.4ab386a129124154c594.js 585 KiB [emitted] [immutable] (name: app)
//...SKIP

이제 기존에 작성했던 아래의 파일들과 패키지는 필요없으니 삭제해준다.

  • <project_root_dir>/src/server.ts
yarn remove express

그리고 폴더 구조를 아래와 같이 다시 재구성해주었다. 폴더 구조 수정에 따라 webpack.config.js에서도 맞춰 수정해주어야한다.

.
├── README.md
├── babel.config.js
├── package.json
├── public
│ └── index.html
├── src
│ ├── index.ts
│ └── styles
│ └── index.css
├── tsconfig.json
├── webpack.config.js
└── yarn.lock

그리고 package.json의 스크립트도 아래와 같이 수정해주었다.

"dev": "webpack serve --mode=development",
"build": "webpack --mode=production"

Additional Webpack Plugins

ProgressPlugin

빌드 진행 상태를 progress로 표현하여 준다.

// webpack.config.js
//...SKIP
const childProcess = require("child_process");

//...SKIP
plugins: [
new webpack.ProgressPlugin({
// handler(percentage, message, ...args) {
// // custom logic
// },
}),
//...SKIP
],
//...SKIP

BannerPlugin

번들된 결과물에 빌드 정보나 커밋 버전등의 내용을 추가 할 때 사용하는 플러그인으로, webpack에서 기본 제공을 해주므로 별도 설치할 필요는 없다.

// webpack.config.js
//...SKIP
const childProcess = require("child_process");

//...SKIP
plugins: [
new webpack.BannerPlugin({
banner:`
Build Data: ${new Date().toLocaleString()}
Commit Version: ${childProcess.execSync('git rev-parse --short HEAD')}
Author: ${childProcess.execSync('git config user.name')}
`
}),
//...SKIP
],
//...SKIP

References

기타 참고글