programming

번들러(Bundler)

번들러에 대해 알아봅시다

thumbnail

번들러(Bundler)는 소프트웨어 개발에서 다양한 파일과 리소스를 하나의 파일로 묶어주는 도구입니다. 웹 개발에서는 여러 개의 JavaScript, CSS 파일, 이미지, 글꼴 등을 하나의 파일(또는 여러 파일)로 합쳐 네트워크 요청 수를 줄이고 로드 속도를 개선합니다. 웹 개발에서 대표적인 번들러는 Webpack, Rollup, Parcel 등이 있습니다.

번들러의 탄생

번들러가 만들어지게 된 계기를 이해하기 위해 모듈 시스템의 역사를 간단히 살펴보는 것이 좋습니다.

초기 웹 개발은 HTML, CSS, JavaScript를 별도의 파일로 관리했고, 각 파일을 <script> 태그와 <link> 태그를 통해 HTML 파일에 포함하여 활용했습니다. 이러한 관리 방식은 두 가지 문제를 야기했습니다.

  • 각 파일의 개별적 네트워크 요청으로 인해 로드 시간이 길어져 성능 문제가 발생했습니다.
  • 여러 파일 간 의존성을 수동으로 관리하고, 올바른 순서로 로드해야 했기에 의존성 관리가 복잡했습니다.
  • 글로벌 네임스페이스를 공유하는 여러 스크립트 파일이 변수 이름 충돌을 일으켰습니다.

이러한 문제를 해결하기 위해 모듈 시스템이 도입됩니다. 이를 통해 코드를 작은 단위로 분리하고 모듈 간의 의존성을 명시적으로 관리할 수 있었습니다. 초기엔 서버에선 CommonJS를 클라이언트에선 AMD(Asynchronous Module Definition)를 활용하다 추후 공식 JavaScript 모듈 시스템인 ES6 모듈을 도입했습니다.

callout_icon

CommonJS, AMD가 있는데 ES6 모듈 시스템은 왜 만들었을까?

JavaScript 생태계에 표준화된 모듈 시스템이 없었기에 개발자들은 서로 다른 모듈 형식을 변환하거나 두 시스템 간의 호환성을 맞추기 위한 추가적인 작업을 해야 했습니다. 그렇기에 동기(CommonJS)와 비동기(AMD) 로딩의 통합이 이루어진 모듈 시스템인 ES6 모듈 시스템이 탄생하게 되었습니다.

모듈 시스템을 적용한 이후에도 문제가 있었습니다. 여전히 파일 로드로 인한 네트워크 요청이 많았고, 사용되지 않는 코드 문제나 코드 압축 및 최적화 문제도 남아있었습니다. 이를 해결하기 위해 번들러가 만들어졌습니다.

번들러는 모든 모듈의 의존성 그래프를 만들고 이를 하나 혹은 하나 이상의 번들 파일로 결합하여 네트워크 요청 문제를 해결했습니다. 또한, 코드를 압축하고 사용하지 않는 코드를 제거하여 파일 크기를 줄였으며 다양한 파일 형식을 처리할 수 있는 시스템을 구축할 수 있게 되었습니다. 이렇게 번들러는 현대 웹 개발의 필수 도구가 되었습니다.

요약

  • 초기 웹 개발은 모든 리소스가 별도의 파일로 관리되어 네트워크 요청, 의존성 관리, 글로벌 네임스페이스 공유 등의 문제가 있었습니다.
  • 이를 위해 모듈 시스템을 도입했으며 CommonJS, AMD 부터 ES6 모듈까지 발전하게 되었습니다.
  • 모듈 시스템의 도입으로 일부 문제가 해결되었지만 여전히 복잡한 파일 로딩과 사용되지 않은 코드, 코드 압축 및 최적화 등의 문제가 남았습니다.
  • 번들러의 탄생은 앞의 문제들을 해결했으며, 현대 웹 개발의 필수 도구가 되었습니다.

번들러의 동작

번들러는 크게 다음과 같은 순서로 동작하며 그 기능을 수행합니다. 이를 설명하기 위해 앞서 언급된 Webpack을 예시로 들어보겠습니다.

  1. 엔트리 포인트 설정
  2. 의존성 그래프 생성
  3. 모듈 처리 및 반환
  4. 번들 생성
  5. 최적화
  6. 출력

엔트리 포인트 설정

번들러의 동작은 진입점 파일(entry point)에서 시작되며, Webpack에서는 config의 entry 속성을 통해 이를 지정할 수 있습니다.

// webpack.config.js
 
module.exports = {
  entry: "./src/index.js",
  /* other configuration */
};

의존성 그래프 생성

지정된 진입점 파일에서 시작하여 해당 파일이 의존하는 모든 모듈을 재귀적으로 추적합니다. 그런 다음, 추적된 모든 모듈을 의존성 그래프로 구성하고, 모듈 간의 관계를 파악하여 파일을 결합할 순서를 결정합니다.

callout_icon

문맥 내 모듈(Module)의 의미

여기서 모듈은 JavaScript와 CSS 외에도 이미지 파일, 글꼴, HTML 파일 등을 포함할 수 있습니다. 이러한 다양한 유형의 파일을 처리하기 위해 Webpack에서는

로더(loader)를 사용하여 이미지 및 글꼴 등을 JavaScript 모듈로 변환합니다.

모듈 처리 및 반환

번들러는 각 모듈을 처리하고 변환하는데, 이 과정에서 로더나 플러그인(plugin)을 활용합니다. 예를 들어, Webpack에서는 babel-loader 를 사용해 ES6 코드를 ES5 코드로 변환하거나, css-loader 를 사용해 CSS 파일을 JavaScript 모듈로 변환할 수 있습니다.

// webpack.config.js
 
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: "asset/resource",
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: "asset/resource",
      },
      {
        test: /\.html$/,
        use: [
          {
            loader: "html-loader",
            options: {
              minimize: true,
            },
          },
        ],
      },
    ],
  },
};

번들 생성

모든 모듈이 처리되면, 번들러는 이들을 파일 형태로 결합합니다. 결과 파일에는 모든 의존성 모듈이 포함되며, 이를 통해 웹 애플리케이션을 로드할 수 있습니다.

단일 파일 번들링의 경우, 모든 코드가 하나의 파일에 포함되지만, 코드 스플리팅(Code Splitting)을 사용하면 여러 파일로 나누어 로드할 수 있습니다. 각각의 설정은 다음과 같이 가능합니다.

// 단일 파일 번들링
module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "bundle.js", // 하나의 번들 파일
    path: path.resolve(__dirname, "dist"),
  },
};
 
// 다중 파일 번들링 (코드 스플리팅 사용)
module.exports = {
  entry: {
    main: "./src/index.js", // 메인 엔트리 포인트
    vendor: "./src/vendor.js", // 별도의 엔트리 포인트
  },
  output: {
    filename: "[name].bundle.js", // 엔트리 포인트별로 별도의 번들 파일 생성
    path: path.resolve(__dirname, "dist"),
  },
  optimization: {
    splitChunks: {
      chunks: "all", // 모든 청크를 분리
    },
  },
};

단일 파일 번들링은 배포와 관리가 간단하고, 네트워크 요청 수가 적어 작은 애플리케이션에 유리합니다. 하지만 앱이 커질수록 파일 크기가 커지고, 전체 파일을 다시 다운로드해야 하며, 초기 로드 시간이 길어질 수 있습니다. 메모리 사용도 비효율적일 수 있습니다.

다중 파일 번들링은 필요한 부분만 로드하여 초기 로드 시간이 짧고, 변경된 부분만 다시 다운로드하면 되므로 캐싱이 효율적입니다. 메모리 사용도 최적화되고, 사용자 경험이 개선됩니다. 하지만 설정과 관리가 복잡하고, 추가 파일 로드로 네트워크 요청 수가 증가할 수 있으며, 로딩 상태 관리가 필요합니다.

callout_icon

코드 스플리팅(Code Splitting) 이해하기

코드 스플리팅은 웹 애플리케이션 성능 최적화를 위한 기법으로, 전체 코드를 여러 개의 작은 번들로 분할하는 것을 의미합니다. 이로써 초기 로드 시간은 감소하고 동적 로드(Dynamic Loading)이 가능하기에 전반적인 UX를 개선할 수 있습니다.

최적화

번들러는 코드 압축(Code Minification), 트리 쉐이킹(Tree Shaking), 코드 스플리팅 등의 최적화 작업을 수행합니다. 코드 압축은 코드 크기를 줄이기 위해 불필요한 요소를 제거하는 과정이고, 트리 쉐이킹은 사용되지 않는 코드를 제거하여 번들 크기를 줄이는 기술입니다. 코드 스플리팅은 앞에서 언급되니 생략하겠습니다.

Webpack의 경우 v4부터 production 모드에서 기본적으로 코드 압축을 적용하고 이를 위해 TerserPlugin 을 활용합니다. 코드 압축이 적용된 압축 코드는 공백 제거, 변수 이름 축약, 불필요한 코드 제거, 문법 최적화 등이 적용되어 파일 크기를 줄어듭니다. 트리 쉐이킹 또한 production 모드에서 동작하며 ES6 모듈 사용, package.jsonoptimization 설정을 추가하여 적용할 수 있습니다. 트리 쉐이킹을 적용하면 import 되지 않은 불필요한 코드를 최종 번들에서 제외하여 최종 번들 크기를 줄일 수 있습니다.

callout_icon

트리 쉐이킹이 ES6 모듈이어야 하는 이유

트리 쉐이킹은 ES6 모듈 시스템의 정적 구조를 통해 가능하기 때문에 이를 적용해야 합니다. 정적 구조는 모듈과 의존성을 컴파일 타임에 결정할 수 있는 특성을 가지며, 이는 동적 구조를 갖는 CommonJS 모듈(require)과 대비됩니다. 간단한 예시를 들어보겠습니다.

// util.js
exports.add = function (a, b) {
  return a + b;
};
 
exports.subtract = function (a, b) {
  return a - b;
};
 
// index.js
const util = require("./util");
 
if (process.env.NODE_ENV === "development") {
  console.log(util.add(2, 3));
} else {
  console.log(util.subtract(5, 2));
}

위 예제 코드에서는 util.addutil.subtract 함수 중 어느 함수가 사용될지는 런타임에 결정됩니다. 이러한 경우, 실제로 사용되는 코드를 미리 알 수 없기 때문에 트리 쉐이킹을 적용하기 어렵습니다. 추가로, 트리 쉐이킹에 특화된 번들러로는 앞서 언급한 Rollup이 있습니다.

출력

최종 번들은 지정된 출력 디렉토리에 저장됩니다. Webpack에서는 output 속성을 사용해 출력 경로와 파일명을 설정할 수 있습니다.

// webpack.config.js
const path = require("path");
 
// 간단한 설정
module.exports = {
  output: {
    filename: "bundle.js", // 번들 파일 이름
    path: path.resolve(__dirname, "dist"), // 출력 디렉토리 절대 경로
    publicPath: "/", // 모든 출력 파일에 대한 기본 경로
  },
};
 
// 상세 설정
module.exports = {
  output: {
    filename: "[name].[contenthash].js", // 청크 이름과 컨텐츠 해시를 사용한 파일 이름
    path: path.resolve(__dirname, "dist"), // 출력 디렉토리 절대 경로
    publicPath: "/", // 웹팩 애셋의 기본 경로 설정
    chunkFilename: "[name].[contenthash].js", // 청크 파일 이름 설정
    asset모듈Filename: "assets/[hash][ext][query]", // 에셋 모듈 파일 이름 설정
    clean: true, // 빌드 시 출력 디렉토리 정리
  },
};

번들러의 역할

번들러의 역할은 앞서 말했던 모듈 번들링, 의존성 관리, 코드 최적화, 다양한 파일 형식 처리를 포함하여 개발 편의성 향상, 빌드 및 배포 자동화가 있습니다. 앞서 언급했던 내용들은 간단한 정리 수준으로 작성하겠습니다.

모듈 번들링

여러 개의 모듈을 하나 혹은 복수의 파일로 결합하여 관리할 수 있도록 합니다. 이를 통해 네트워크 요청을 최소화 할 수 있고, 파일 로딩 순서를 관리하여 애플리케이션 초기 로딩 속도를 개선합니다.

의존성 관리

모듈 간의 의존성 문제를 의존성 그래프를 통해 각 모듈이 어떻게 의존하는지, 모듈의 결합 순서는 어떻게 되는지 파악하여 필요한 모듈을 올바른 순서로 로드할 수 있도록 해줍니다. 이를 통해 개발자가 의존성 관리를 더 쉽게 할 수 있도록 돕습니다.

코드 최적화

코드 압축, 트리 쉐이킹을 통해 코드 최적화를 할 수 있습니다. 트리 쉐이킹을 통해 사용하지 않는 코드를 제거하여 최종 번들 파일 크기를 줄이고, 코드 압축을 통해 파일 크기를 최소화합니다. 이를 통해 파일 크기가 줄어 네트워크 시간을 단축하고 클라이언트 로드 및 실행 속도를 개선하여 애플리케이션 성능을 향상시킬 수 있습니다.

다양한 파일 형식 처리

JavaScript 파일 뿐만 아니라 CSS, 이미지, 글꼴 등 다양한 파일 형식을 처리하고 최적화 할 수 있습니다. 이를 통해 다양한 파일 형식을 효율적으로 관리할 수 있고 애플리케이션 성능을 최적화 할 수 있습니다.

개발 편의성 향상

번들러의 탄생을 통해 HMR(Hot Module Replacement)을 활용하여 실시간으로 변경사항을 적용할 수 있게 되었습니다. 이를 통해 개발 중 빠른 피드백을 제공하여 개발 생산성을 증가시킬 수 있습니다.

callout_icon

HMR에 대하여

HMR은 개발을 진행하면서 코드 변경 시 전체 페이지를 리로드하지 않고 변경된 모듈만을 실시간으로 교체하는 기능입니다. 이를 통해 입력 데이터, 스크롤 위치 등 상태를 잃지 않고 개발을 계속 진행할 수 있으며, 변경될 모듈만을 교체하기 때문에 불필요한 로딩과 렌더링을 줄여 성능을 최적화 할 수 있습니다.

Webpack의 경우 파일 시스템을 감시하여 소스 파일 변경을 감지하고 변경된 모듈을 다시 번들링합니다. 이후 웹 소켓(WebSocket)을 통해 연결된 브라우저에 변경된 모듈에 대한 정보를 전달합니다. 이를 수신한 브라우저는 HMR 메시지를 처리하고 변경된 모듈 만을 교체하여 실시간 수정이 가능하게 됩니다.

빌드 및 배포 자동화

빌드 및 배포 자동화는 번들러 자체 기능이라기보다는, 번들러가 제공하는 아티팩트를 활용하여 CI/CD 파이프라인에서 수행되는 작업을 의미합니다. 따라서 빌드 및 배포 자동화를 지원하는 방식으로 동작하며, 실제로 빌드 및 배포 자동화는 CI/CD 도구와의 통합을 통해 이루어집니다.

Webpack의 경우 각종 플러그인과 스크립트를 통해 빌드 및 배포 자동화를 지원합니다.

번들러 선택에 고민하는 당신에게

웹 개발을 진행할 때, 초기 설정 도구를 사용하여 프로젝트를 설정하는 경우가 많으며, 이 도구들은 대부분 기본적으로 번들러 설정을 포함하고 있습니다. 덕분에 번들러 선택에 대한 고민은 많지 않지만, 만약 번들러 선택을 고민하는 단계라면 다음 사항을 고려해볼 수 있습니다.

앞서 언급한 대표적인 번들러인 Webpack, Rollup, Parcel를 예시로 비교해보도록 하겠습니다.

Webpack

Webpack은 공식 문서에서 "모던 JavaScript 애플리케이션을 위한 정적 모듈 번들러"로 소개하고 있습니다. Webpack은 v4 이후로는 기본 설정을 필요로 하지 않아 초기 설정이 간단해졌으며, 다양한 플러그인과 로더를 통해 다양한 추가 기능을 활용할 수 있습니다. 또한 코드 스플리팅, 트리 쉐이킹 등 고급 최적화 기술을 활용할 수 있으며 큰 커뮤니티와 풍부한 문서들은 학습에도 큰 도움이 될 수 있습니다.

만약 당신이 고급 최적화 기능을 필요로 하는 대규모 애플리케이션을 계획하고 계신다면 Webpack을 추천드립니다.

Rollup

Rollup은 공식 문서에서 "작은 코드 조각을 라이브러리나 애플리케이션과 같이 더 크고 복잡한 것으로 컴파일하는 JavaScript용 모듈 번들러입니다"라고 설명하고 있습니다. ES6 모듈을 기반으로 최적화된 번들을 생성하는 모듈 번들러이며 트리 쉐이킹에 특화되어 있습니다. 다양한 플러그인을 활용해 기능을 확장할 수 있지만 설정이 복잡하고 브라우저 환경에서는 다른 번들러보다 강력하지 않을 수 있습니다.

만약 당신이 번들 최소화를 필요로 하는 라이브러리를 계획하고 계신다면 Rollup을 추천드립니다.

Parcel

Parcel은 공식 문서에서 "즉시 사용 가능한 훌륭한 개발 경험과 확장 가능한 아키텍처를 결합하여 프로젝트를 시작 단계에서 대규모 프로덕션 애플리케이션으로 전환할 수 있도록 해줍니다"라고 설명하고 있습니다. 초기에 Zero Config라는 점을 강조하며 빠른 개발을 진행할 수 있다는 점이 강점이었습니다. 또한 Parcel 2부터는 플러그인 시스템을 도입하여 확장성을 개선함으로써 대규모 프로젝트에도 적합하도록 발전하고 있습니다. 하지만 Parcel의 생태계 및 커뮤니티는 다른 번들러에 비해 작으며, 디버깅 도구가 충분하지 않다는 점은 한계가 될 수 있습니다.

만약 당신이 빠른 학습을 필요로 하는 프로토타입 개발이나 소규모 웹 애플리케이션 개발을 목표로 하고 있다면 Parcel을 추천드립니다.