bun 환경에서 image를 minimize 할 때 생긴 문제로 node 환경으로 migration 한 뒤에 이전에 포스팅 했던 기본적인 웹팩 설정들을 했다.
yarn init
typescript 패키지 설치
yarn add -D typescript ts-node @types/node
webpack 패키지 설치
yarn add -D webpack webpack-cli webpack-dev-server @types/webpack @types/webpack-dev-server
webpack.config.ts 만들기
touch webpack.config.ts
// webpack.config.ts
import { Configuration } from "webpack";
const config: Configuration = {
entry: "./src/index.ts",
};
export default config;
todo_app 에서 src와 images를 가져 왔는데, app의 엔트리포인트가 ./src/index.ts이기 때문에 작성해 준다
// webpack.config.ts
import path from "path";
import { Configuration } from "webpack";
const config: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main.js",
},
};
export default config;
현재 webpack.config 파일이 루트 디렉터리에 있으므로, ./dist/main.js 가 만들어지게끔 설정했다.
패키지 설치
yarn add -D ts-loader
ts 파일과 tsx 파일을 resolvable 한 ext로 만들기 위해 resolve 옵션 추가
// webpack.config.ts
import path from "path";
import { Configuration } from "webpack";
const config: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main.js",
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
}
};
export default config;
ts-loader 설정
// webpack.config.ts
import path from "path";
import { Configuration } from "webpack";
const config: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main.js",
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
],
},
};
export default config;
에러 전문
[webpack-cli] Failed to load 'webpack.config.ts' config
[webpack-cli] TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for webpack.config.ts
at new NodeError (node:internal/errors:405:5)
at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:99:9)
at defaultGetFormat (node:internal/modules/esm/get_format:142:36)
at defaultLoad (node:internal/modules/esm/load:120:20)
at ModuleLoader.load (node:internal/modules/esm/loader:388:13)
at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:270:56)
at new ModuleJob (node:internal/modules/esm/module_job:65:26)
at ModuleLoader.#createModuleJob (node:internal/modules/esm/loader:282:17)
at ModuleLoader.getJobFromResolveResult (node:internal/modules/esm/loader:240:34)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:221:17) {
code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
타입스크립트를 node 환경에서 실행 가능하게 해 주는 ts-node 패키지가 설치되어 있음에도 ts 파일 로드를 실패한다면 package.json에 'type: module' 이 설정되어 있는지 확인한 후, 이를 제거해 준다.
제거 후, import 문에서 에러가 날 것이다
에러 전문
[webpack-cli] Failed to load 'webpack.config.ts' config
[webpack-cli] webpack.config.ts:1
import path from "path";
^^^^^^
SyntaxError: Cannot use import statement outside a module
at internalCompileFunction (node:internal/vm:73:18)
at wrapSafe (node:internal/modules/cjs/loader:1153:20)
at Module._compile (node:internal/modules/cjs/loader:1205:27)
at Module.m._compile (/Users/joepasss/joepasss/webpack/node_modules/ts-node/src/index.ts:1618:23)
at Module._extensions..js (node:internal/modules/cjs/loader:1295:10)
at Object.require.extensions.<computed> [as .ts] (/Users/joepasss/joepasss/webpack/node_modules/ts-node/src/index.ts:1621:12)
at Module.load (node:internal/modules/cjs/loader:1091:32)
at Function.Module._load (node:internal/modules/cjs/loader:938:12)
at Module.require (node:internal/modules/cjs/loader:1115:19)
at require (node:internal/modules/helpers:130:18)
import statement를 module 밖에서 사용할 수 없다는 에러가 뜨는데, tsconfig.json 파일에서 module이 "CommonJS"가 아닌 다른 종류로 되어 있는지 확인하고, 만일 다른 종류로 설정되어 있다면 CommonJS로 바꿔준다.
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
...
},
}
수정이 완료된 후 yarn webpack
을 실행해 보면, 정상적으로 잘 되는 것을 볼 수 있다.
production 환경에서 사용할 config과 dev 환경에서 config을 분리 해 주고, 공통적으로 필요한 config을 만들어준다.
폴더, 파일 생성
mkdir webpack; touch ./webpack/webpack.common.config.ts ./webpack/webpack.prod.config.ts ./webpack/webpack.dev.config.ts
common.config 작성
// webpack.common.config.ts
import path from "path";
import { Configuration } from "webpack";
const commonConfig: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "main.js",
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
],
},
};
export default commonConfig;
기존에 작성한 webpack.config.ts 와 거의 동일하다. 위치가 root 디렉터리에서 /webpack 폴더로 바뀌었으므로, output의 path 속성을 ./dist
에서 ../dist
로 바꿔준다
기존 작성한 webpack.config.ts는 이제 사용하지 않으므로 삭제해 준다 rm -r ./webpack.config.ts
webpack config을 합치려면 webpack-merge 패키지가 필요하므로 설치해 준다. yarn add -D webpack-merge
prod.config 작성
// webpack.prod.config.ts
import { Configuration } from "webpack";
import commonConfig from "./webpack.common.config";
import { merge } from "webpack-merge";
const prodConfig: Configuration = merge(commonConfig, {
mode: "production",
});
export default prodConfig;
dev.config 작성
import { Configuration } from "webpack";
import commonConfig from "./webpack.common.config";
import { merge } from "webpack-merge";
const devConfig: Configuration = merge(commonConfig, {
mode: "development",
});
export default devConfig;
prod.config 과 dev.config을 실행할 script를 package.json에 작성해 준다
// package.json
{
...
"scripts": {
"build": "webpack --config ./webpack/webpack.prod.config.ts",
"dev": "webpack --config ./webpack/webpack.dev.config.ts"
}
...
}
이후 yarn run build
yarn run dev
또는 yarn dev
명령어로 실행되는 모습을 볼 수 있다.
src 폴더에 있는 index.html을 템플릿 삼아, 번들링된 js 파일과 css 파일을 자동으로 import 해 보자
웹팩은 기본적으로 js파일과 json 파일을 제외하면 이해하지 못하기 때문에 html을 load 해 주는 loader가 필요하다
패키지 설치
yarn add -D html-loader
로더 적용
// webpack.common.config.ts
import path from "path";
import { Configuration } from "webpack";
const commonConfig: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "main.js",
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
{
test: /\.html$/,
loader: "html-loader",
},
],
},
};
export default commonConfig;
템플릿을 기반으로 html을 생성해 주는 플러그인인, html-webpack-plugin도 설치해 준다
yarn add -D html-webpack-plugin
플러그인 적용
// webpack.common.config.ts
import path from "path";
import { Configuration } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
const commonConfig: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "main.js",
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
{
test: /\.html$/,
loader: "html-loader",
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "src/index.html",
filename: "index.html",
}),
],
};
export default commonConfig;
이후 index.html에 명시적으로 import된 js, css 를 삭제한다
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple Todo App</title>
</head>
<body>
<div class="container">
<img
class="header-image"
src="../images/header-image.jpg"
alt="To Do List"
/>
<h1>To Do List</h1>
<div class="todolist-wrapper">
<input class="new-todo" placeholder="Enter text here" autofocus />
<ul class="todo-list"></ul>
</div>
</div>
</body>
</html>
yarn run build
를 실행한 뒤 생성된 index.html 파일을 보면, <script defer="defer" src="main.js"></script>
가 생성된 것을 볼 수 있다.
index.ts 파일에 sass import
//src/index.ts
...
import "./style.scss";
...
이후, yarn run build
를 해 보면 Error: Can't resolve './styles.scss'
에러가 뜬다.
sass는 css의 전처리기(preprocessor)이므로 sass 처리 -> css 처리 -> Style 태그 안에 삽입 이 필요하다.
sass처리는 sass-loader가 담당하고, css 처리는 css 로더가, style 태그 안에 삽입은 style-loader가 해 준다.
패키지 설치
yarn add -D sass sass-loader css-loader style-loader
loader 적용
// webpack.common.config.ts
import path from "path";
import { Configuration } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
const commonConfig: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "main.js",
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
{
test: /\.html$/,
loader: "html-loader",
},
{
test: /\.(scss|sass)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "src/index.html",
filename: "index.html",
}),
],
};
export default commonConfig;
css는 사용하지 않을 예정이므로, 따로 설정해주지 않는다.
이후 yarn run build
를 실행해 보면, css 파일이 따로 나오는게 아닌, js 파일에 통합된 걸 볼 수 있는데, css 파일을 mini-css-extract-plugin 으로 추출 해 보자
mini-css-extract-plugin을 사용하려면, style-loader가 아닌 MiniCssExtractPlugin.loader를 사용해야 하는데, MiniCssExtractPlugin.loader는 style-loader와 혼용이 불가능하다. 그래서, 기존에 common.config 에 작성한 css 관련 loader 옵션을 dev.config으로 옮긴 뒤 prod.config을 작성해야 한다.
common.config
// webpack.common.config.ts
import path from "path";
import { Configuration } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
const commonConfig: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "main.js",
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
{
test: /\.html$/,
loader: "html-loader",
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "src/index.html",
filename: "index.html",
}),
],
};
export default commonConfig;
dev.config
// webpack.dev.config.ts
import { Configuration } from "webpack";
import commonConfig from "./webpack.common.config";
import { merge } from "webpack-merge";
const devConfig: Configuration = merge(commonConfig, {
mode: "development",
module: {
rules: [
{
test: /\.(scss|sass)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
],
},
});
export default devConfig;
prod.config
// webpack.prod.config.ts
import { Configuration } from "webpack";
import commonConfig from "./webpack.common.config";
import { merge } from "webpack-merge";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
const prodConfig: Configuration = merge(commonConfig, {
mode: "production",
module: {
rules: [
{
test: /\.(scss|sass)$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
],
},
plugins: [new MiniCssExtractPlugin()],
});
export default prodConfig;
production 모드에서, 번들링되는 파일의 이름이 항상 같기 때문에, 캐싱 문제가 발생 할 수 있다. 브라우저에서 같은 이름이기 때문에 캐싱된 파일을 가져와서, 빌드된 최신 결과를 확인하지 못 할 수 있기 때문에, 생성되는 파일의 이름에 hash 값을 추가해 캐싱 문제를 해결해야 한다
common.config 에서 output 옵션에 들어간 filename을 dev.config으로 옮겨준다.
common.config
// webpack.common.config.ts
import path from "path";
import { Configuration } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
const commonConfig: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "../dist"),
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
{
test: /\.html$/,
loader: "html-loader",
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "src/index.html",
filename: "index.html",
}),
],
};
export default commonConfig;
dev.config
// webpack.dev.config.ts
import { Configuration } from "webpack";
import commonConfig from "./webpack.common.config";
import { merge } from "webpack-merge";
const devConfig: Configuration = merge(commonConfig, {
mode: "development",
output: {
filename: "main.js",
},
module: {
rules: [
{
test: /\.(scss|sass)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
],
},
});
export default devConfig;
이제 prod.config 에서 생성되는 js, css에 hash 값을 추가한다
prod.config
// webpack.prod.config.ts
import { Configuration } from "webpack";
import commonConfig from "./webpack.common.config";
import { merge } from "webpack-merge";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
const prodConfig: Configuration = merge(commonConfig, {
mode: "production",
output: {
filename: "js/[name].[contenthash:12].js",
},
module: {
rules: [
{
test: /\.(scss|sass)$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].[contenthash:12].css",
}),
],
});
export default prodConfig;
js의 경우, output 프로퍼티로 해쉬를 추가했고, css는 MiniCssExtractPlugin을 이용해 추가했다. 컨텐츠가 바뀔 때 마다 해쉬를 새로 작성하라는 의미에서 contenthash를 사용했다.
이후, yarn run build
를 해 보면, 생성이 되지만, 이전에 생성된 파일을 수동으로 삭제해줘야 하는 불편함이 있는데, clean-webpack-plugin을 이용해서 자동으로 이전 빌드된 파일들을 삭제해 줄 수 있다.
yarn add -D clean-webpack-plugin
으로 설치 후, plugins 에 추가만 해 주면 된다.
common.config
// webpack.common.config.ts
import path from "path";
import { Configuration } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import { CleanWebpackPlugin } from "clean-webpack-plugin";
const commonConfig: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "../dist"),
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
{
test: /\.html$/,
loader: "html-loader",
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "src/index.html",
filename: "index.html",
}),
new CleanWebpackPlugin(),
],
};
export default commonConfig;
이후, yarn run build
또는 yarn dev
를 실행해 보면, 자동으로 이전에 빌드된 파일이 삭제되는 것을 볼 수 있다.
css minimizer plugin을 통해 번들링된 css 를 미니마이즈 해 보자,
yarn add -D css-minimizer-webpack-plugin
으로 패키지를 설치한 후, prod.config에 적용
prod.config
// webpack.prod.config.ts
import { Configuration } from "webpack";
import commonConfig from "./webpack.common.config";
import { merge } from "webpack-merge";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import CssMinimizerPlugin from "css-minimizer-webpack-plugin";
const prodConfig: Configuration = merge(commonConfig, {
mode: "production",
output: {
filename: "js/[name].[contenthash:12].js",
},
optimization: {
minimize: true,
minimizer: [
`...`,
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
"default",
{
discardComments: { removeAll: true },
},
],
},
}),
],
},
module: {
rules: [
{
test: /\.(scss|sass)$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].[contenthash:12].css",
}),
],
});
export default prodConfig;
기존 Configuration 인터페이스에서는 devServer 프로퍼티가 없으므로, 타입을 새로 작성해 준다
import { Configuration as WebpackConfiguration } from "webpack";
import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'
interface Configuration extends WebpackConfiguration {
devServer?: WebpackDevServerConfiguration;
}
dev.config
// webpack.dev.config.ts
import { Configuration as WebpackConfiguration } from "webpack";
import { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
import commonConfig from "./webpack.common.config";
import { merge } from "webpack-merge";
import path from "path";
interface Configuration extends WebpackConfiguration {
devServer?: WebpackDevServerConfiguration;
}
const devConfig: Configuration = merge(commonConfig, {
mode: "development",
output: {
filename: "main.js",
},
devServer: {
port: 9000,
static: path.resolve(__dirname, "../dist"),
devMiddleware: {
index: "index.html",
writeToDisk: true,
},
client: {
overlay: true,
},
liveReload: true,
},
module: {
rules: [
{
test: /\.(scss|sass)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
],
},
});
export default devConfig;
이후, package.json 에 써놓은 dev 스크립트를 수정해야 한다. dev server를 열기 위해서는 webpack 명령이 아닌 webpack serve 명령을 이용해야 하기 때문에 webpack --config ./webpack/webpack.dev.config.ts
에서 webpack serve --config ./webpack/webpack.dev.config.ts
로 바꿔준다
// package.json
{
...
"scripts": {
"build": "webpack --config ./webpack/webpack.prod.config.ts",
"dev": "webpack serve --config ./webpack/webpack.dev.config.ts"
}
}