본문으로 바로 가기
로고
개발

[JS] 프레임워크 없이 SPA 만들기 - 5 (번들링)

읽는 시간 14분
[JS] 프레임워크 없이 SPA 만들기 - 5 (번들링) 글의 썸네일"

#들어가며

웹팩(Webpack)은 모듈 번들러로, 여러 개의 자바스크립트 파일, 스타일시트, 이미지 등을 하나의 파일로 묶어 웹의 성능을 향상시키는 도구다. 이번 포스팅에서 웹팩을 우리가 진행중인 SPA 프로젝트에 적용하는 방법과 이로 인해 얻을 수 있는 이점에 대해 알아보자.

#웹팩 전용 전

우선 우리가 이전까지 개발했던 코드를 실행해서 네트워크 탭을 한 번 살펴보자.

브라우저의 네트워크 탭, 많은 js 파일을 불러오고 있는 모습
브라우저의 네트워크 탭, 많은 js 파일을 불러오고 있는 모습

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

#웹팩 적용 후

웹팩을 통해 bundle.js 하나로 합친 모습
웹팩을 통해 bundle.js 하나로 합친 모습

웹팩을 적용하면 많은 JavaScript파일을 하나의 JavaScript파일(bundle.js)로 통합해서 로드된다. 네트워크 요청 수가 크게 줄어든 모습을 볼 수 있다.

#번들링?

번들링은 여러 개의 파일과 모듈을 하나 또는 여러 개의 파일로 합치는 과정이다. 웹팩 외에도 Vite나 Rollup, ESBuild, 터보팩 등 다양한 번들러가 존재한다.

#설치

웹팩을 사용하기 위해서는 먼저 웹팩과 웹팩 CLI를 설치해야 한다. 그럼 아래 명령어로 웹팩을 먼저 설치해보자.

shell
npm install --save-dev webpack webpack-cli
shell
npm install --save-dev webpack webpack-cli
  • webpack 의 역할

    1. 모듈 번들링: 웹팩은 여러 개의 파일과 모듈을 하나 또는 몇 개의 파일로 번들링해주므로, 네트워크 요청을 줄일 수 있다.
    2. 최적화와 Minification: 불필요한 코드를 제거하고, 파일을 압축하여 로딩 시간을 단축시켜준다.
  • webpack-cli 의 역할

    1. 터미널 사용 용이: 웹팩 CLI는 터미널에서 웹팩을 쉽게 사용할 수 있도록 도와준다. 이를 통해 webpack <명령어>와 같은 형식으로 웹팩 명령어를 실행할 수 있다.
    2. 커스터마이징: CLI를 통해 웹팩 설정 파일을 커스터마이징하며, 다양한 옵션과 플러그인을 적용할 수 있다.

프로덕션 환경에서는 필요없기 때문에 개발 의존성으로 설치했다.

#웹팩 설정 파일 생성

웹팩의 모든 설정은 webpack.config.js 파일에서 이루어진다. 이 파일에서 로더, 플러그인, 번들링 될 진입점(entry point) 등을 정의하게 된다.

프로젝트 root 폴더에 webpack.config.js 파일을 생성해보자.

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"), // 번들된 파일의 위치
  }
};
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"), // 번들된 파일의 위치
  }
};
  • entry: 웹팩이 파일을 읽어들이기 시작하는 부분이다. 여기서부터 필요한 모듈을 로딩하고 하나의 파일로 묶는다.
  • output: 웹팩이 생성하는 파일의 이름과 저장 경로를 지정한다.
    • filename: 출력 파일의 이름, 여기서는 bundle.js 라는 이름의 파일이 생성된다.
    • publicPath: 웹 서버에서 이용될 때 파일 경로, 여기서는 /static/ 경로를 사용한다.
    • path: 번들링된 결과물이 저장될 실제 경로
    • 빌드 후 우리는 bundle.js 라는 이름의 파일이 frontend/dist 폴더에 생길 것이다.
  • mode: 웹팩의 모드 설정, 주로 development, production, none 중 하나를 사용한다.

#로더와 플러그인

#로더의 개념과 역할

로더는 웹팩이 웹을 해석할 때 JS 파일이 아닌 웹 자원(HTML, CSS, 이미지 등)들을 변환할 수 있도록 도와주는 속성이다. 로더는 파일을 해석하고 변환하는 과정에 관여하며, 다양한 옵션을 설정하여 파일 처리 방식을 커스텀 할 수 있다.

#CSS로더 설정

로더는 module 에서 설정한다.

js
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
};
js
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 도 설치)

shell
npm install --save-dev mini-css-extract-plugin css-loader
shell
npm install --save-dev mini-css-extract-plugin css-loader

#플러그인 개념과 역할

플러그인은 웹팩의 기본적인 동작에 추가적인 기능을 제공한다. 로더가 파일 단위로 처리한다면, 플러그인은 번들된 결과물을 조작한다. 예를 들어, 압축, 미니피케이션, 환경변수 주입 등을 플러그인을 통해 할 수 있다. 위에서 사용한 MiniCssExtractPlugin.loader(로더)는 MiniCssExtractPlugin 플러그인 내부에 있다. 그래서 이 plugin을 사용한다는걸 webpack에게 알려줘야한다.

#플러그인 설정

js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 
module.exports = {
  //name
  //mode
  //devtool
 
  //entry
 
  //module
 
  plugins: [
    new MiniCssExtractPlugin({
      filename: "styles.css", // 추출할 파일의 이름
    }),
  ],
 
  //output
 
  //devServer
};
js
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 를 사용하기 위해 개발 의존성으로 설치해주자

shell
npm install --save-dev webpack-dev-server
shell
npm install --save-dev webpack-dev-server

설치가 완료되면 webpack.config.js에 devServer 옵션을 추가한다.

js
const path = require('path');
 
module.exports = {
  // ... 기타 설정 ...
  devServer: {
    devMiddleware: {
      publicPath: "/static/", // 빌드 결과물이 서빙될 경로
    },
    static: {
      directory: path.join(__dirname, "frontend"), // 정적 파일을 제공할 디렉토리
    },
    port: 9000, // 포트번호
  },
};
js
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 옵션을 주고 해봐도 새로고침을 막을 순 없었다 ㅠㅠ.. (공부가 더 필요한 부분)

#프로젝트 소스코드 수정

이 상태에서 아래 명령어를 터미널에 입력하면

shell
webpack --config webpack.config.js
shell
webpack --config webpack.config.js

dist 폴더에 bundle.js와 styles.css가 생성되는 모습을 확인할 수 있다.

우리 프로젝트 소스코드를 조금씩 수정해서 실제로 bundle.js를 사용하도록 만들어보자!

그 전에 명령어를 좀 더 간편하게 사용하기 위해 package.json 파일에서 scripts 를 추가해보았다.

json
 // package.json 의 scripts 부분
 
 "scripts": {
    "dev": "webpack-dev-server --open --mode development",
    "build": "webpack --config webpack.config.js",
    "start": "npm run build && node server.js"
  },
json
 // 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를 사용하기 위해서 몇 가지 파일에서 수정할 부분이다.

index.html
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>
index.html
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>
index.js
js
import { router } from "./router.js";
import "../css/style.css"; // 여기
 
window.addEventListener("popstate", router);
 
document.addEventListener("DOMContentLoaded", () => {
  router();
});
index.js
js
import { router } from "./router.js";
import "../css/style.css"; // 여기
 
window.addEventListener("popstate", router);
 
document.addEventListener("DOMContentLoaded", () => {
  router();
});
server.js
js
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..."));
server.js
js
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..."));

#마치며

이번 포스팅을 작성하면서, 여태 무심코 사용해왔던 다양한 마법(?)들에 대해서 공부할 수 있었다. 마치 마법같던 여러 기능들이 어떻게 작동하는지, 그리고 이를 어떻게 활용할 수 있는지 조금이나마 이해할 수 있는 시간이었다. 웹팩의 설정이나 로더, 플러그인 등등 작동 원리를 이해함으로써, 우리도 이러한 '마법'을 자유자재로 다룰 수 있는 마법사가 되어보자! ㅋㅋ

웹팩은 실제로 깊게 배우기에는 상당히 복잡하고 방대한 주제라고 생각한다.. 모든 것을 한 번에 알필요는 없기에 필요한 부분부터 차근차근 학습하면서 활용해보는 것이 중요한 것 같다. 조금 조금씩 시간을 내서 공부해봐야겠다.

이 포스팅이 나같은 병아리 개발자에게 조금이라도 도움이 되었으면 좋겠다. 화이팅?

#참고자료

Concepts | 웹팩 (webpack.kr)

React 기본 강좌 2-3. 웹팩 설치하기 - YouTube