[JS] 프레임워크 없이 SPA 만들기 - 5 (번들링)
![[JS] 프레임워크 없이 SPA 만들기 - 5 (번들링) 글의 썸네일"](/_next/image?url=%2Fassets%2Fimages%2Fposts%2F2023%2F09%2Fspa-5%2F1.gif&w=2048&q=75)
#들어가며
웹팩(Webpack)은 모듈 번들러로, 여러 개의 자바스크립트 파일, 스타일시트, 이미지 등을 하나의 파일로 묶어 웹의 성능을 향상시키는 도구다. 이번 포스팅에서 웹팩을 우리가 진행중인 SPA 프로젝트에 적용하는 방법과 이로 인해 얻을 수 있는 이점에 대해 알아보자.
#웹팩 전용 전
우선 우리가 이전까지 개발했던 코드를 실행해서 네트워크 탭을 한 번 살펴보자.

웹팩을 적용하지 않았을 땐, 다수의 JavaScript 파일이 개별적으로 로드되고 있다. 만약 컴포넌트가 많다면 그에 맞게 로딩되는 JavaScript 파일 수도 늘어날 것이다.
#웹팩 적용 후

웹팩을 적용하면 많은 JavaScript파일을 하나의 JavaScript파일(bundle.js)로 통합해서 로드된다. 네트워크 요청 수가 크게 줄어든 모습을 볼 수 있다.
#번들링?
번들링은 여러 개의 파일과 모듈을 하나 또는 여러 개의 파일로 합치는 과정이다. 웹팩 외에도 Vite나 Rollup, ESBuild, 터보팩 등 다양한 번들러가 존재한다.
#설치
웹팩을 사용하기 위해서는 먼저 웹팩과 웹팩 CLI를 설치해야 한다. 그럼 아래 명령어로 웹팩을 먼저 설치해보자.
npm install --save-dev webpack webpack-cli
npm install --save-dev webpack webpack-cli
-
webpack 의 역할
- 모듈 번들링: 웹팩은 여러 개의 파일과 모듈을 하나 또는 몇 개의 파일로 번들링해주므로, 네트워크 요청을 줄일 수 있다.
- 최적화와 Minification: 불필요한 코드를 제거하고, 파일을 압축하여 로딩 시간을 단축시켜준다.
-
webpack-cli 의 역할
- 터미널 사용 용이: 웹팩 CLI는 터미널에서 웹팩을 쉽게 사용할 수 있도록 도와준다. 이를 통해
webpack <명령어>
와 같은 형식으로 웹팩 명령어를 실행할 수 있다. - 커스터마이징: CLI를 통해 웹팩 설정 파일을 커스터마이징하며, 다양한 옵션과 플러그인을 적용할 수 있다.
- 터미널 사용 용이: 웹팩 CLI는 터미널에서 웹팩을 쉽게 사용할 수 있도록 도와준다. 이를 통해
프로덕션 환경에서는 필요없기 때문에 개발 의존성으로 설치했다.
#웹팩 설정 파일 생성
웹팩의 모든 설정은 webpack.config.js 파일에서 이루어진다. 이 파일에서 로더, 플러그인, 번들링 될 진입점(entry point) 등을 정의하게 된다.
프로젝트 root 폴더에 webpack.config.js 파일을 생성해보자.
const path = require("path");
module.exports = {
name: "my-first-webpack", // 없어도 됨 ( 그냥 어떤 설정인지 쓰는 용도 )
mode: "development", // 실서비스에선 production
devtool: "eval", // 소스맵 생성 방법 production에선 source-map 이나 cheap-module-source-map
entry: "./frontend/static/js/index.js", // 엔트리 포인트(입구)
//module(아래에서)
//plugin(설명)
output: { // 출구
filename: "bundle.js", // 번들된 파일 이름
publicPath: "/static/",
path: path.resolve(__dirname, "frontend", "dist"), // 번들된 파일의 위치
}
};
const path = require("path");
module.exports = {
name: "my-first-webpack", // 없어도 됨 ( 그냥 어떤 설정인지 쓰는 용도 )
mode: "development", // 실서비스에선 production
devtool: "eval", // 소스맵 생성 방법 production에선 source-map 이나 cheap-module-source-map
entry: "./frontend/static/js/index.js", // 엔트리 포인트(입구)
//module(아래에서)
//plugin(설명)
output: { // 출구
filename: "bundle.js", // 번들된 파일 이름
publicPath: "/static/",
path: path.resolve(__dirname, "frontend", "dist"), // 번들된 파일의 위치
}
};
- entry: 웹팩이 파일을 읽어들이기 시작하는 부분이다. 여기서부터 필요한 모듈을 로딩하고 하나의 파일로 묶는다.
- output: 웹팩이 생성하는 파일의 이름과 저장 경로를 지정한다.
- filename: 출력 파일의 이름, 여기서는 bundle.js 라는 이름의 파일이 생성된다.
- publicPath: 웹 서버에서 이용될 때 파일 경로, 여기서는 /static/ 경로를 사용한다.
- http://도메인/static/bundle.js 로 접근할 수 있다.
- path: 번들링된 결과물이 저장될 실제 경로
- 빌드 후 우리는 bundle.js 라는 이름의 파일이 frontend/dist 폴더에 생길 것이다.
- mode: 웹팩의 모드 설정, 주로 development, production, none 중 하나를 사용한다.
#로더와 플러그인
#로더의 개념과 역할
로더는 웹팩이 웹을 해석할 때 JS 파일이 아닌 웹 자원(HTML, CSS, 이미지 등)들을 변환할 수 있도록 도와주는 속성이다. 로더는 파일을 해석하고 변환하는 과정에 관여하며, 다양한 옵션을 설정하여 파일 처리 방식을 커스텀 할 수 있다.
#CSS로더 설정
로더는 module 에서 설정한다.
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//name
//mode
//devtool
//entry
module: {
rules: [ // 각각의 로더를 정의한다.(그래서 배열)
{
test: /\.css$/, // test속성에선 정규표현식을 이용해서 파일 확장자를 지정한다.
use: [MiniCssExtractPlugin.loader, "css-loader"], // test에서 지정한 유형의 파일에 적용할 로더들(뒤에서 부터 적용)
},
// 필요에 따라 다른 로더 추가
],
},
//plugins
//output
//devServer
};
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//name
//mode
//devtool
//entry
module: {
rules: [ // 각각의 로더를 정의한다.(그래서 배열)
{
test: /\.css$/, // test속성에선 정규표현식을 이용해서 파일 확장자를 지정한다.
use: [MiniCssExtractPlugin.loader, "css-loader"], // test에서 지정한 유형의 파일에 적용할 로더들(뒤에서 부터 적용)
},
// 필요에 따라 다른 로더 추가
],
},
//plugins
//output
//devServer
};
css-loader: CSS 파일을 CommonJS 모듈로 해석해서 JavaScript파일에서 import 구문을 통해 css를 불러올 수 있게 된다.
MiniCssExtractPlugin.loader: 여기서는 style-loader 대신 사용했다. 별도의 css파일을 build 후에 만들고 싶어서 사용했다. style-loader는 css를 js번들에 인라인으로 삽입하지만, 이걸 쓰면 파일으로 추출한다.
MiniCssExtractPlugin.loader를 사용하기 위해서는 아래 명령어로 플러그인을 설치해야한다. (css-loader 도 설치)
npm install --save-dev mini-css-extract-plugin css-loader
npm install --save-dev mini-css-extract-plugin css-loader
#플러그인 개념과 역할
플러그인은 웹팩의 기본적인 동작에 추가적인 기능을 제공한다. 로더가 파일 단위로 처리한다면, 플러그인은 번들된 결과물을 조작한다. 예를 들어, 압축, 미니피케이션, 환경변수 주입 등을 플러그인을 통해 할 수 있다. 위에서 사용한 MiniCssExtractPlugin.loader(로더)는 MiniCssExtractPlugin 플러그인 내부에 있다. 그래서 이 plugin을 사용한다는걸 webpack에게 알려줘야한다.
#플러그인 설정
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//name
//mode
//devtool
//entry
//module
plugins: [
new MiniCssExtractPlugin({
filename: "styles.css", // 추출할 파일의 이름
}),
],
//output
//devServer
};
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//name
//mode
//devtool
//entry
//module
plugins: [
new MiniCssExtractPlugin({
filename: "styles.css", // 추출할 파일의 이름
}),
],
//output
//devServer
};
이렇게 설정해두면 dist 폴더에 styles.css가 생성될 것이다.
#웹팩 Dev Server 설정
#필요성과 역할
웹팩 Dev Server는 개발 과정에서 매우 유용한 도구다. create-react-app으로 리액트 프로젝트를 만들면 코드를 변경해서 저장하면 화면에 바로 반영되는 그것이다. 로컬 개발 환경에서 간단한 웹 서버를 제공하며, 라이브 리로딩 기능을 통해 소스 코드 변경 시 자동으로 브라우저를 새로고침 해준다. 이렇게 함으로써 개발자는 변경사항을 즉시 확인할 수 있어 개발 효율성이 크게 향상된다.
#설치 및 설정
웹팩 Dev Server 를 사용하기 위해 개발 의존성으로 설치해주자
npm install --save-dev webpack-dev-server
npm install --save-dev webpack-dev-server
설치가 완료되면 webpack.config.js에 devServer 옵션을 추가한다.
const path = require('path');
module.exports = {
// ... 기타 설정 ...
devServer: {
devMiddleware: {
publicPath: "/static/", // 빌드 결과물이 서빙될 경로
},
static: {
directory: path.join(__dirname, "frontend"), // 정적 파일을 제공할 디렉토리
},
port: 9000, // 포트번호
},
};
const path = require('path');
module.exports = {
// ... 기타 설정 ...
devServer: {
devMiddleware: {
publicPath: "/static/", // 빌드 결과물이 서빙될 경로
},
static: {
directory: path.join(__dirname, "frontend"), // 정적 파일을 제공할 디렉토리
},
port: 9000, // 포트번호
},
};
#HMR(Hot Module Replacement)
HMR은 중요한 기능 중 하나인데, 변경된 모듈만 교체해서 페이지를 다시 불러오지 않고도 업데이트 할 수 있게 해준다. input창에 뭔가 값을 적어놓고 테스트중에 다른 코드를 변경했을 때, input의 value는 그대로이면서 변경된 부분이 페이지 새로고침 없이 수정되는 것이다.
근데.. 몇 시간 구글링해보고 했지만.. hot: true 옵션을 주고 해봐도 새로고침을 막을 순 없었다 ㅠㅠ.. (공부가 더 필요한 부분)
#프로젝트 소스코드 수정
이 상태에서 아래 명령어를 터미널에 입력하면
webpack --config webpack.config.js
webpack --config webpack.config.js
dist 폴더에 bundle.js와 styles.css가 생성되는 모습을 확인할 수 있다.
우리 프로젝트 소스코드를 조금씩 수정해서 실제로 bundle.js를 사용하도록 만들어보자!
그 전에 명령어를 좀 더 간편하게 사용하기 위해 package.json 파일에서 scripts 를 추가해보았다.
// package.json 의 scripts 부분
"scripts": {
"dev": "webpack-dev-server --open --mode development",
"build": "webpack --config webpack.config.js",
"start": "npm run build && node server.js"
},
// package.json 의 scripts 부분
"scripts": {
"dev": "webpack-dev-server --open --mode development",
"build": "webpack --config webpack.config.js",
"start": "npm run build && node server.js"
},
dev 명령어에는 개발서버를, build는 웹팩을 적용한 파일을 만드는 명령어를 입력해두었고, start는 빌드한 결과물을 실행해 볼 수 있도록 설정했다. (여기서 env를 설정하면 웹팩 설정파일에서 개발, 배포에 따라서 다른 설정을 할 수도 있다.)
아래는 번들된 파일과 css를 사용하기 위해서 몇 가지 파일에서 수정할 부분이다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
name="viewport">
<meta content="ie=edge" http-equiv="X-UA-Compatible">
<title>Document</title>
<link href="/static/styles.css" rel="stylesheet">
<script src="/static/bundle.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
name="viewport">
<meta content="ie=edge" http-equiv="X-UA-Compatible">
<title>Document</title>
<link href="/static/styles.css" rel="stylesheet">
<script src="/static/bundle.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
import { router } from "./router.js";
import "../css/style.css"; // 여기
window.addEventListener("popstate", router);
document.addEventListener("DOMContentLoaded", () => {
router();
});
import { router } from "./router.js";
import "../css/style.css"; // 여기
window.addEventListener("popstate", router);
document.addEventListener("DOMContentLoaded", () => {
router();
});
const express = require("express");
const path = require("path");
const app = express();
app.use((req, res, next) => {
console.log("Request URL:", req.originalUrl);
next();
});
app.use("/static", express.static(path.resolve(__dirname, "frontend", "dist"))); // dist로 변경
app.get("/*", (req, res) => {
res.sendFile(path.resolve(__dirname, "frontend", "index.html"));
});
app.listen(process.env.PORT || 3000, () => console.log("Server running..."));
const express = require("express");
const path = require("path");
const app = express();
app.use((req, res, next) => {
console.log("Request URL:", req.originalUrl);
next();
});
app.use("/static", express.static(path.resolve(__dirname, "frontend", "dist"))); // dist로 변경
app.get("/*", (req, res) => {
res.sendFile(path.resolve(__dirname, "frontend", "index.html"));
});
app.listen(process.env.PORT || 3000, () => console.log("Server running..."));
#마치며
이번 포스팅을 작성하면서, 여태 무심코 사용해왔던 다양한 마법(?)들에 대해서 공부할 수 있었다. 마치 마법같던 여러 기능들이 어떻게 작동하는지, 그리고 이를 어떻게 활용할 수 있는지 조금이나마 이해할 수 있는 시간이었다. 웹팩의 설정이나 로더, 플러그인 등등 작동 원리를 이해함으로써, 우리도 이러한 '마법'을 자유자재로 다룰 수 있는 마법사가 되어보자! ㅋㅋ
웹팩은 실제로 깊게 배우기에는 상당히 복잡하고 방대한 주제라고 생각한다.. 모든 것을 한 번에 알필요는 없기에 필요한 부분부터 차근차근 학습하면서 활용해보는 것이 중요한 것 같다. 조금 조금씩 시간을 내서 공부해봐야겠다.
이 포스팅이 나같은 병아리 개발자에게 조금이라도 도움이 되었으면 좋겠다. 화이팅?