본문 바로가기

Web

번들러를 알아보자

도입

최근 협업 프로젝트를 시작했다..

알다시피 최근 react의 cra가 deprecated됐다는 소식이 있었다.

부끄럽지만 지금까지 난 cra가 뭔지도 잘 모르는 상태로

아~무 생각이나 의심없이 그냥 create-react-app 하면 리액트 앱이 편하게 깔렸고 개발하고...

 

그리고 최근 CRA가 deprecated되면서 어떻게 리액트 앱을 빌드할지 찾아보게 됐다.

그리고 vite라는 걸 알게 됐고, 자연스레 기존의 웹팩과 비교를 하게 됐고, 번들러란 무엇인지.. 더 나아가 모듈 발전의 역사까지 공부하게 됐다... 

 

사실 번들러를 공부하면서 너무 헷갈렸던 게, 웹팩이나 비트 등등 여러 도구들을 비교하려 하니 서로 차이점이 명확하게 와닿지 않았다. 그러니까 번들러라곤 하지만 기능적으로 보면 아예 빌드 도구라고 해야하는 게 아닌지,,, 

공부하면 할 수록 이 번들러라는 것들이 번들을 넘어 빌드, 컴파일 등 폭넓게 기능들을 제공하고 있었다. 

몇몇 번들러는 다른 번들러를 이용하기도 했다(?).

 

한참을 헤매다 다행히 번들러에 관해 시리즈로 너무 잘 정리된 블로그 글을 읽게 됐다. (아래 링크 참고)

https://deemmun.tistory.com/86  

 

번들러 파헤치기 1 - 모듈 시스템의 발전과 역사 (commonJS, AMD, UMD, ESM-esmodule)

개인적으로 클라이언트 환경에서 가장 진입장벽이 높게 느껴지는 부분은 빌드 환경인 것 같습니다.다른 부분은 사실 실무에서도 자주 다루고 접하다 보니 금방 익숙해지는 반면, 프론트엔드의

deemmun.tistory.com

 

해당 시리즈를 바탕으로,

각 도구들이 어떤 부분에 초점을 두고 어떤 문제점을 해결하고 있는지에 중점을 두며 각각의 특징들을 정리해봤다. 


번들러(budler)

웹 애플리케이션을 개발하기 위해 필요한 HTML, CSS, JS 등 여러개로 파편화된(모듈화된) 자원들을 모아서 하나 혹은 최적의 소수 파일로 결합(bundling)해주는 도구

 


번들러 등장 배경

 

번들러의 등장은 자바스크립트의 모듈 시스템 발전과 깊은 관련이 있다. 

 

모듈 시스템이 탄생하기 전, javascript 파일은 html 파일의 <script> 태그를 통해 불러와졌다.

이렇게 불러온 여러개의 각 파일들은 결국 하나의 전역 스코프에서 실행됐고,

각 파일(=모듈)에서 선언된 변수나 함수가 서로 영향을 주고받는 등의 문제가 발생했다.

 

이런 문제들을 해결하고자 모듈화를 위한 다양한 움직임들이 있었고,

CommonJS에서 시작해 AMD, UMD 등의 모듈 시스템이 등장했다.

이러한 모듈 시스템을 효율적으로 브라우저에서 사용하기 위해 번들러가 등장하게 된다.

 

나중엔 ECMAScript2015 표준 명세에 ECMA modules이 등장하며, 자바스크립트 자체 모듈 시스템을 사용할 수 있게 됐다.

(자바스크립트 모듈 표준의 등장)

 

번들러의 발전

 

 

대표적인 번들러로 Webpack, Vite, Rollup 등이 있음


용어 정리 


Tree-Shaking (트리 쉐이킹)

  • 사용하지 않는 불필요한 코드를 최종 빌드 결과물에서 제거하여 용량을 최적화하는 기법
     

HMR (Hot Module Replacement)

  • 코드의 수정이 발생할 경우 새로고침 없이도 바로 변경된 부분을 확인할 수 있는 개발 효율성을 크게 향상시키는 중요한 기능
  • 코드의 수정이 발생하면 런타임에 이를 알리고 강제로 새로고침을 진행하는 Hot-Reloading 개념을 모듈에 적용시켜 확장한 개념.

code-spliting (코드 스플리팅, 코드 분할, 번들 분할)

  • 번들을 모두 하나로 통합하는 것이 아닌, lazy-loading을 통해 필요한 시점에 분리된 번들을 요청하는 식으로 번들을 나누어 개별 번들 사이즈를 줄이는 방식

Webpack

  • 단순한 파일 통합을 넘어서 개발 편의 기능을 제공하는 정적 모듈 번들러
  • 웹팩은 CSS, 폰트, 이미지, JS를 하나의 파일로 압축하여 관리함으로써 네트워크 요청 수를 줄이고, Tree-Shaking 기법을 통한 빌드 결과물 최적화, HMR (Hot Module Replacement), code-spliting 기능 제공
  • 개발 서버 기능인 webpack-dev-server가 존재 -> HMR 지원
    • `webpack-dev-server`는 개발 중인 웹 애플리케이션을 위한 HTTP 서버를 제공하며, HMR 기능을 통해 코드 변경 시 페이지 새로고침 없이 변경된 모듈을 실시간으로 교체할 수 있게 한다. 이를 통해 개발자는 빠르게 변경 사항을 보고, 애플리케이션의 상태를 유지하면서 개발할 수 있다. 
  • 초기 설정이 복잡하여 러닝 커브가 높은 편
  • but 강력한 로더와 플러그인 시스템을 통해 다양한 라이브러리와 여러 파일 형식과의 호환성을 제공
  • 많은 로더와 플러그인을 구성해야하며, 이로 인해 큰 프로젝트에서는 구성 파일이 매우 방대해지며 빌드 시간이 길어질 수 있다.
  • 초기 웹팩이 릴리즈 되는 시점은 esmodule 표준이 채택되기 전으로 commonjs인 cjs 포맷만을 지원했다는 특징이 있습니다. (2020.10 릴리즈 된 v5에 esmodule 포맷 지원이 추가되었습니다.)

Webpack 설정파일을 뜯어보자

 

웹팩은 webpack.config.js 파일을 통해 프로젝트의 구성을 설정한다.

기본적으로 다음과 같은 옵션이 있다.

  • 진입점(Entry)
  • 출력(Output)
  • 로더(Loaders)
  • 플러그인(Plugins)
module.exports = {
  entry: '', // 진입점
  output: {}, // 출력
  module: { // 로더(모듈)
    rules: []
  },
  plugins: [], // 플러그인
  // 기타 옵션...
}

 

진입점(Entry)

진입점은 웹팩이 프로젝트를 해석하기 시작하는 파일을 말한다.

module.exports = {
  entry: './src/main.js'
}

 

필요한 경우 다중 진입점을 사용할 수도 있다.

module.exports = {
  entry: {
    main: './src/main.js',
    sub: './src/sub.js',
    hi: './src/hi.js',
    // ...
  }
}

 

 

출력(Output)

출력은 웹팩이 생성하는 번들 파일의 이름과 경로를 설정하는 옵션이다.

module.exports = {
  output: {
    // 기본값,
    // path: path.resolve(__dirname, 'dist'), // 현재 파일의 절대 경로를 기준, dist 폴더에 결과 생성
    // filename: '[name].js', // [name]을 통해 진입점의 파일 이름이 그대로 적용
    clean: true // 결과 생성 전, 기존 결과물 삭제
  }
}

 

* __dirname은 Node.js CommonJS 모듈 시스템에서 제공하는 전역 변수로, 현재 파일의 절대 경로를 반환한다.

* dist는 distribution(배포)의 약어로, 배포될 최종 결과물이 담긴 폴더를 의미한다.

 

로더(Loader)

로더는 웹팩이 *.js 파일 외 기타 파일들을 처리하는 기능으로 module 속성을 통해 구성한다.

module.rules 속성에 배열 형태로 작성하며, 각 규칙은 기본적으로 test, use 속성을 가진다.

test 속성에 어떤 파일들을 처리할지 지정하고, 정규표현식으로 작성한다.

use 속성에선 해당 파일을 처리할 로더를 명시한다.

대부분의 로더는 npm i -D <name>으로 설치하는 외부 패키지다.

 

다음은 scss, css, jsx 등의 파일들과 여러 이미지 파일을 처리할 수 있도록 구성한 예제이다.

npm i -D style-loader css-loader sass-loader sass swc-loader @swc/core

 

module.exports = {
  module: {
    rules: [
      {
        test: /\.s?css$/,
        use: [
          // 순서 중요!
          'style-loader',
          'css-loader',
          'sass-loader'
        ]
      },
      {
        test: /\.jsx?$/,
        use: 'swc-loader'
      },
      {
        test: /\.(png|jpe?g|svg|gif|webp)$/i, // i 옵션 - 대소문자 구분 없음
        type: 'asset/resource' // type을 통해 Webpack에 내장된 로더(Builtin Loader) 명시
      }
    ]
  }
}

 

플러그인(PlugIn)

플러그인은 웹팩의 기본적인 동작에 추가적인 기능을 제공하는 속성이다.

 

다음은 번들링된 결과물을 HTML 파일에 연결하고, static 폴더의 파일을 복사해 결과에 추가하고, React를 전역 변수로 사용할 수 있도록 설정한 예제이다.

npm i -D html-webpack-plugin copy-webpack-plugin

 

html-webpack-plugin: 최초 실행될 HTML 파일을 연결

copy-webpack-plugin: 정적 파일(파비콘, 이미지 등)을 제품(dist) 폴더로 복사

 

const HtmlPlugin = require('html-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new CopyPlugin({
      patterns: [{ from: 'static' }]
    })
  ]
}

 

... 이러한 복잡한 설정으로 인해 러닝 커브가 높다는 걸 다시 한 번 실감할 수 있었다.


Rollup.JS

: ESM(esmodule) 지원 번들러의 등장 - 2015

  • 번들 사이즈 경량화와 번들 최적화를 중점에 둔 esm 지원 모듈 번들러
  • 따라서 라이브러리구현에 많이 쓰임
  • 웹팩 이후에 등장한 모듈 번들러로서 당시 웹팩에는 없던 esm을 지원하며 의존성 파악이 명확하여 사용하지 않는 코드를 제거하는 Tree-shaking을 더 강력하게 지원
  • 다양한 번들 포맷과 Tree-shaking, code-spliting을 제공
  • 자체적으로 HMR 기능이 구현X - 플러그인 rollup-plugin-hot을 통해 HMR 사용 가능

** cjs는 module.exports 객체를 통해 모듈을 정의하고, require 함수를 통해 모듈 로더가 동기적으로 불러온다.
반면 esm의 경우 import, export 구문을 통해 모듈을 정의하고 import를 통해 비동기적으로 모듈을 불러온다.

 

module.exports 구문은 객체를 통해 관리된다. 객체가 갖는 특성때문에 모듈 내에서 실행흐름에 따라 module.exports의 값을 동적으로 바꿀 수 있다. 그렇기에 모듈을 실행한 뒤에야 반환 값을 알 수 있으며, 모듈 간 의존성을 파악하는 게 구조적으로 어렵다는 문제가 있다.

 

반면 esm은 import, export 구문을 통해 내보낼 모듈, 불러올 모듈을 명확히 파악할 수 있다 보니 모듈 간 의존성 파악을 명확히 할 수 있다. 이 덕분에 사용하지 않는 코드를 제거하는 tree-shaking 같은 최적화 기법을 더 강력하게 지원할 수 있다. 

더보기
commonJs vs ES Module

/*commonJS*/

/* test.js */
module.exports = 'hello'

/* index.js */
const hello = require('./test.js')​
/*ESModule*/

/* test.js */
const hello = 'hello'
export default hello;

/* index.js */
import hello from './test.js'


1. 문법 및 사용법

- CommonJS: module.exports 객체를 통해 모듈을 정의하고, require 함수를 통해 모듈 로더가 동기적으로 불러온다.
- ES Module: import와 export 문법을 사용하여 모듈을 관리한다.

2. 동작 방식

- CommonJS: 런타임에 모듈이 분석된다. require() 호출 시 모듈을 로드하고 실행한다. 이 동작은 동기적으로 작동된다. ( -> 따라서 브라우저에서 사용하기에는 성능 이슈 발생 가능)
- ES Module: 모듈은 정적으로 분석되며, 빌드 타임 또는 로드 타임에 미리 결정된다. import 문을 통한 동작은 비동기적으로 동작 가능하다.

3. 호환성
- CommonJS: Node.js 환경에서 기본적으로 사용되며, 브라우저 환경에서는 Webpack, Browserify 등의 도구로 번들링해야한다.
- ES Module: 브라우저와 Node.js에서 기본적으로 지원된다.(Node.js에서 ESM을 사용하려면 package.json 파일에 "type": "module" 지정) ES6 이후의 최신 표준을 사용하는 환경에서 사용이 유리하다. 구형 환경에서는 Babel과 같은 트랜스파일러나 번들러가 필요하다. 또한 기존 CJS를 사용한 코드와의 호환성 문제가 발생할 수 있으며, 두 모듈 시스템을 혼합해서 사용할 때 추가적인 설정이 필요하다. 

 


parcel

zero-configuration 번들러의 등장

  • The zero configuration tool: webpack과 Rollup와 같은 복잡한 설정 없이 바로 사용할 수 있는 번들러
  • 낮은 러닝 커브
  •  멀티 코어 처리를 활용하여 빠른 빌드 시간 제공
  • 파일 시스템 캐시를 사용하여 재빌드 시간 단축
  • HMR 지원
  • 좀 더 복잡한 프로젝트에서 필요한 세밀한 최적화나 커스터마이징에 한계가 존재
  • 프로젝트의 규모가 커질수록 파일 수와 의존성이 급격히 증가 -> 캐시 관리와 멀티 코어 리소스를 효율적으로 사용하는 데에 어려움 발생
  • 소규모 프로젝트에 적합

esbuild

100배 빠른 번들러의 등장

An extremely fast bundler for the web
The main goal of the esbuild bundler project is to bring about a new era of build toll performance, and create an easy-to-use modern bundler along the way

 

esbuild 번들러는 빌드 도구 성능에 새로운 시대를 열고, 사용하기 쉬운 번들러를 만들기 위해 탄생했습니다.

주요 기능

  • 캐싱이 필요 없는 최고의 속도
  • JavaScript, CSS, TypeScript, JSX 내장
  • ESM과 CommonJS 모듈 번들 지원
  • CSS 모듈을 포함한 CSS 번들 생성
  • Tree shaking, 번들 minification, source map 지원
  • local server, watch mode, plugins

왜 빠른가?

컴파일 언어인 GO로 구현

Go 언어는 코어 자체가 병렬 처리를 위해 설계되었기 때문에 필요한 스레드를 만들고 여러 작업을 동시에 처리할 수 있다.

또한 Go 언어로 구현된 esbuild는 CPU를 최대한 많이 사용하는 방향으로 설계되었으며, CPU 캐시를 적극 사용하도록 하여 JS를 파싱해 만드는 구문 분석 트리 AST를 캐시에 최대한 오래 머물토록 하여 속도를 높였다.

 

다만,

아직 1.0.0. 버전에 도달하지 못했고, 아직 es5를 100% 지원하지 않는다.

 

Rollup.js: rollup-plugin-esbuild

Webpack: esbuild-roader

 

esbuild의 빠른 번들링과 축소화를 rollup과 webpack 번들러에서도 플러그인과 로더를 통해 사용할 수 있다.


Snowpack

빠른 웹 개발을 위한 프론트엔드 빌드 툴의 등장

 

 

Snowpack is a lightning-fast frontend build tool 

 

snowpack은 ESM을 활용하고 개발 서버에 중점을 둔 빌드 툴로, Unbundled Development를 핵심 개념으로 갖고 있다.

 

 

Unbundled Development

웹팩이나 롤업과 같은 기존 번들러들은 개발 중 코드의 수정이 발생하면 변경된 파일을 다시 빌드하고, 전체 파일에 대한 번들링을 진행한다.

 

스노우팩은 코드의 변경이 발생할 경우 해당 파일만 다시 빌드하고, 캐시로 갖고있도록 구현하였다.

 

번들없는 개발은 개별 파일을 브라우저에 전달하는 아이디어다. Babel, TypeScript, Sass 같은 자주 사용하는 라이브러리로 파일을 빌드하고, ESM import/export 구문으로 브라우저에서 개별적으로 로드한다.
그러니깐 개발할때 코드를 하나로 합치지 않고, 각각의 파일을 그대로 브라우저에 보내는 방식이다.

필요한 파일만 빠르게 바꿔서 바로바로 결과를 볼 수 있는 개발 방식이다.

 

번들링을 아예 하지 말아야한다는 게 아니라, 개발 시엔 번들링이 불필요하다는 방식이다.

 

스노우팩은 실제 프로덕션을 위한 빌드 결과물은 웹팩, 롤업과 같은 번들러를 사용할 수 있도록 하고, 개발 중일 때는 빠른 빌드가 가능한 esbuild를 채택해 개발 경험을 개선했다.

 

 

** 현재 사실상 지원 종료를 알리며 Vite를 대안으로 추천하고 있다.


 

Vite

Vite은 ESM을 이용한 개발서버와 Rollup 최적화 빌드 커맨드를 제공하는 프론트엔드 빌드 툴

 

Snowpack is also a no-bundle native ESM dev server that is very similar in scope to Vite.

 

자바스크립트 어플리케이션은 점점 더 많은 기능을 필요로 했고, 그로 인해 필요한 모듈의 수도 점점 늘어나면서 개발 서버의 구동 시간과 코드의 수정이 브라우저에 반영되는 시간이 느려지는 문제가 있었다.

Vite는 이러한 느린 개발 서버와 느린 업데이트 문제를 해결하고자 했다..

 

먼저 개발환경에서 vite의 성능 최적화 전략을 보자.

Vite는 개발환경에서 pre-bundling 도구로 esbuild를 이용한다.

Vite는 번들하지 않는 ESM 개발 서버를 제공했던 snowpack과 코어 컨셉이 일치하고, 많은 영역에 대해 유사함을 가지고 있다.

 

Dependency pre-bundling


의존성은 개발 중에는 거의 바뀌지 않기 때문에, vite는 이 의존성을 미리 단일 모듈로 번들링한다.

번들링된 결과는 /node_modules/.vite/ 폴더에 저장된다.
처음 개발 서버를 로드할 시 해당 작업을 수행한 후, 변화된 부분이 있는 경우에만 업데이트하도록 한다.

이렇게 함으로써 브라우저가 각각의 의존성 파일들을 따로따로 여러번 HTTP 요청을 하는 게 아니라,
한 번의 HTTP 요청으로 번들된 의존성 파일을 가져온다.

그리고 이 파일들은 캐시되어, 다음 요청부터는 서버가 아닌 브라우저 캐시에서 바로 불러올 수 있다.

import React from 'react';
import { map } from 'lodash';

// Vite pre-bundling 후
import React from '/node_modules/.vite/deps/react.js';
import { map } from '/node_modules/.vite/deps/lodash.js'

 

소스 코드는 ESM을 통해 제공

 

또한 소스코드는 ESM을 통해 제공하여, 따로 번들링 단계없이 페이지에 필요한 파일만 브라우저가 요청하도록 함.

 

정리하면 의존성들은 사전 번들링하고, 나머지 소스코드 파일들은 필요시에만 번들링 거치는 것 없이 브라우저에 로드하도록 해 개발 환경을 개선한 것이다. 

 

이외에도 HTTP 캐싱 활용, HMR을 번들러가 아닌 ESM을 통해 진행할 수 있도록 하는 등 여러 성능 최적화 전략을 적용했다.

 

그리고 프로덕션 환경에서는 번들링 도구로 Rollup을 사용한다.

esbuild는 롤업에 비해 아직 플러그인의 유연성이 부족한 상태기 때문이다.

 

따라서 Vite는 개발 서버는 esbuild를 통해 빠르게, 빌드는 Rollup을 사용해 유연성 있게 설계를 진행하였다.