번들러(Bundler)와 번들링 최적화

 

프론트엔드 개발을 하다 보면 Webpack이나 Parcel 같은 번들러의 이름을 들어본 적이 있을 것이다.

사실 우리는 이 도구들을 설정하고 사용해왔지만 그 역할이나 필요성에 대해 명확히 설명하기는 어렵다.

"그냥 필요하니까 쓰는 거겠지?"라는 생각으로 넘어가는 경우도 많고 나 또한 그래왔다.

하지만 프로젝트 규모가 커지면서 여러 자바스크립트 파일과 CSS, 이미지 등의 리소스를 효율적으로 관리할 필요성을 느끼기 시작한다.

특히 네트워크 요청이 많아질수록 성능 저하와 로딩 속도 문제를 경험하게 된다. 이때 번들러라는 것을 들어보게 될 것이다.

번들러는 이런 문제를 해결하기 위해 등장한 도구로, 모듈 관리와 최적화를 통해 개발 경험을 크게 향상시킨다.

이번 포스팅에서는 번들러가 정확히 무엇인지, 왜 필요한지, 어떤 기능을 제공하는지 등에 대해 살펴보고자 한다.

 

 

01. 번들러(Bundler) 등장 배경

인터넷 초창기 시절에는 웹 서비스의 규모가 작았고, HTML과 자바스크립트 파일의 크기도 비교적 작아 웹 페이지를 유지하는데 큰 어려움이 없었다. 하지만 인터넷이 발달하고 대규모 웹 어플리케이션이 등장하면서 복잡성과 규모가 급격히 증가했다. 이제는 수백개의 자바스크립트 파일, 스타일 시트, 이미지 등이 하나의 웹 어플리케이션을 구성하게 되면서 다양한 문제들이 나타나기 시작했다.

그 문제는 아래와 같다.

 

◼︎ 모듈 간의 네이밍 충돌 문제

대규모 프로젝트에서는 여러 개발자들이 동시에 다양한 자바스크립트 파일을 작성한다.

이 과정에서 변수나 함수 이름이 중복되는 문제가 발생할 수 있다.

예를 들어, hello.js와 world.js 모두 number라는 변수를 사용하고 있다면, Index.html에서 이 변수를 사용할 때 어떤 값을 참조하는지 명확하지 않게 된다. 이러한 네이밍 충돌은 예기치 않은 버그를 유발하며, 대규모 프로젝트에서는 이를 사전에 예방하기 어렵다.

 

◼︎ 파일 전송 문제

브라우저는 URI의 요청에 따라 서버로부터 파일을 가져온다. 이때 웹 어플리케이션을 구성하는 파일이 많다면, 사용자 요청에 대한 응답 시간이 길어질 수 밖에 없다. 예를 들어, 각 파일 요청에 1초가 걸린다고 가정해보자. 이 상황에서 100개의 파일을 로드한다면 100초가 소요되고, 10000개의 파일을 로드한다면 10000초가 소요될 것이다.

 

이렇게 많은 파일을 요청할 경우 로딩 시간이 지나치게 길어져 사용자 경험이 크게 저하된다. 또한, 많은 파일 요청으로 인해 네트워크 비용이 증가하는 문제도 발생할 수 있다. 하나의 큰 자바스크립트 파일로 모든 스크립트를 합치는 방식으로 문제를 해결할 수 있겠지만 이는 유지보수 측면에서 좋지 않다. 모든 코드를 하나의 파일에 넣으면 관리가 어려워지고 코드의 가독성도 떨어지게 될 것이다.

 

 

이러한 문제들을 해결하기 위해 번들러가 등장하였다.

 

 

 

02. 번들러(Bundler)란?

번들러(Bundler)는 여러 개의 파일(자바스크립트, CSS, 이미지 등)을 하나의 파일 또는 최소한의 묶음으로 결합해주는 도구이다.

특히, 웹 어플리케이션의 복잡성과 규모가 커지면서 많은 파일들을 관리하고 최적화하기 위해 등장한 것이 번들러이다.

대표적인 번들러로는 Webpack, Parcel, Rollup, Browerify 등이 있다.

출처: https://webpack.js.org/

 

💡 (Module)번들러의 필요성

◼︎ 네트워크 요청 최소화

웹 어플리케이션이 로딩될 때, 여러 개의 작은 파일을 개별적으로 요청하게 되면 HTTP 요청이 늘어나 네트워크 병목 현상이 발생할 수 있다. 번들링을 통해 여러 파일을 하나로 합치면 HTTP 요청 수가 줄어들어 페이지 로딩 속도가 빨라진다.

 

◼︎ 중속성 관리와 충돌 방지

현대 어플리케이션은 다양한 외부 라이브러리와 모듈을 사용한다. 각각의 모듈 간 종속성을 번들러가 관리해주어 의존성 충돌을 방지할 수 있다. 이를 통해 개발자는 더 구조화된 방식으로 모듈 단위의 코딩이 가능해진다.

 

◼︎ 리소스 최적화

번들러는 자바스크립트 뿐만 아니라 CSS, 이미지, 폰트 등 다양한 리소스도 함께 번들링할 수 있다. 이로 인해 어플리케이션의 리소스 관리가 쉬워지고 최적화된다. 이미지 압축, CSS 주입 등을 통해 필요한 리소스만 로드할 수 있다.

 

◼︎ 코드 압축 및 최적화(Uglify & Minify)

번들링 과정에서 Uglify와 Minify라는 기술을 사용해 코드를 최적화한다.

  • Uglify : 코드 난독화로 변수명과 함수명을 짧게 줄이고 로직을 복잡하게 만들어 코드의 크기를 줄이고 외부에서 쉽게 이해하지 못하도록 함
  • Minify : 공백, 개행, 주석 등을 제거하여 코드의 크기를 최소화

이러한 최적화는 번들 파일의 크기를 줄이고 성능을 높이는데 기여한다.

 

 

 

03. 번들링 과정

 

◼︎ Entry

번들링의 시작점(진입점).

예를 들어, index.js를 Entry로 지정하면 Webpack이 이 파일을 시작으로 의존성을 추적하여 모든 필요한 모듈을 번들링한다.

 

◼︎ Output

번들링된 결과물의 저장 위치와 파일명을 지정.

path는 어디에, filename은 어떤 파일의 이름으로 번들 결과를 저장할 지를 지정한다.

위의 예시에서는 dist라는 폴더에 bundle.js라는 파일 이름으로 번들 결과를 저장하라는 의미이다.

html에서는 script 태그를 이용하여 dist/bundle.js를 가져와서 번들된 결과물을 가져와 로드한다.

 

◼︎ Loader와 Plugin

Webpack은 기본적으로 자바스크립트 파일만 인식한다.

따라서 이미지, CSS, 폰트 등의 리소스를 처리하기 위해 Loader와 Plugin이 필요하다.

 

Loader는 Webpack이 인지하지 못하는 다양한 파일 형식을 읽고 번들링할 수 있도록 도와주는 역할을 한다.

  • css-loader : css 파일을 읽어 수집하고 번들링한다. JS 파일에서 import './style.css'처럼 CSS를 가져올 때 사용된다.
  • style-loader : 읽어들인 CSS를 <style> 태그로 변환하여 head 태그에 주입한다. 브라우저에서 동적으로 스타일을 적용할 때 유용하다. DOM 내 CSS를 바로 주입을 해버려서 CSS 파일을 받아오기 위한 요청의 개수를 줄일 수 있어 페이지의 로딩 속도를 단축할 수 있다. 반면 현재 페이지에서 불필요한 CSS까지도 주입된다. ⇒ 번들을 나누지 않고 하나의 파일로 생성한다거나, 페이지 간에 공유하는 스타일이 많은 경우에 사용하는 것이 좋다.
  • mini-css-extract-plugin : CSS 파일을 추출하여 <link> 태그를 통해 필요한 CSS만 로드하게 한다. style-loader는 <style> 태그를 사용하지만, mini-css-extract-plugin은 별도의 CSS 파일을 생성하여 파일 크기를 줄이고 필요한 경우에만 로드할 수 있게 한다. 별도로 요청을 보내야만 CSS를 불러올 수 있지만 현재 페이지에서만 사용될 CSS만을 불러오기 때문에 불러올 데이터의 크기가 줄어든다는 장점이 있다. ⇒ 코드 스플릿팅을 적용해 번들 파일이 여러 개이거나 페이지 별로 공유하고 있는 CSS의 크기가 크지 않을 때 사용하는 것이 좋다.

Plugin은 Webpack의 동작을 확장하거나 추가적인 기능을 제공한다.

 

◼︎ Chunk

하나의 큰 번들 파일을 여러 개의 작은 파일(Chunk)로 나누어 효율적으로 관리하는 방법이다.

각 페이지 진입점(entry point)마다 chunk로 번들링을 따로 하여, 페이지마다 번들된 chunk 가 생긴다. 예를 들어, 상품 상세보기 페이지에서 필요한 파일만 번들링할 수 있다. 코드 스플릿팅 기법으로 필요한 시점에 필요한 파일만 로드할 수 있다.

 

번들러는 다양한 파일을 결합하고 최적화하여 네트워크 요청을 줄이고, 종속성을 관리하며, 리소스 최적화를 통해 웹 어플리케이션 성능을 향상시킨다.
- Loader는 다양한 형식의 파일을 읽어오고.
- Plugin은 번들링 기능을 확장하며,
- Chunk는 효율적인 리소스 로딩을 돕는다.

 

 

 

 

04. 번들링 최적화

웹 어플리케이션의 성능을 향상시키기 위해서는 번들링 최적화가 필수이다.

Webpack을 비롯한 번들러들은 다양한 최적화 기법을 제공하여 코드 크기를 줄이고 불필요한 리소스 로드를 방지한다.

주요 최적화 기법으로는 Code Splitting, Tree shaking 등이 있다.

 

#1. Code Spliting

코드 스플리팅은 큰 번들 파일을 작은 청크로 나누어, 필요한 시점에 필요한 코드만 로드할 수 있도록 하는 기법이다.

 

(1) Dynamic Import

import() 구문을 사용하여 모듈을 동적으로 불러온다.

이 방식은 비동기로 동작하며, Promise를 반환한다.

예를 들어, 큰 크기의 10MB 자바스크립트 파일을 여러 개의 작은 파일로 나누어 필요할 때만 비동기적으로 로드할 수 있다.

// 동적 가져오기 예시
const loadModule = async () => {
  const { default: module } = await import('./module.js');
  module(); // 함수 호출 시점에 모듈 로드
};

import를 함수처럼 사용하면 해당 모듈은 번들에서 분리되어 필요할 때만 로드된다.

 

 

(2) React에서의 코드 스플리팅: React.lazy와 Suspense

React.lazy는 동적 import를 활용하여 컴포넌트를 비동기로 로드한다.

필요한 시점에 컴포넌트를 비동기로 로드하며, 로드 완료 전까지 Suspense를 사용해 컴포넌트 로드 중 로딩 상태를 표시할 수 있다.

import React, { Suspense } from 'react';

const LazyComponent = React.lazy(() => import('./LazyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

 

 

(3) Lazy Loading과 Pre-Loading

Lazy Loading은 초기 번들에 포함되지 않고 필요할 때만 로드하는 방식이다.

예를 들어, 페이지 진입 시점이 아니라 사용자가 버튼을 클릭하거나 양식을 제출할 때 추가적인 모듈을 로드한다.

const loadComponent = () => {
  import('./heavyComponent.js').then(module => {
    const Component = module.default;
    // 컴포넌트를 사용
  });
};

 

Pre-Loading은 사용자가 다음으로 이동할 가능성이 높은 페이지나 필요한 리소스를 미리 로드하여, 전환 시 지연 시간을 줄인다.

<link rel="preload" > 태그를 사용하여 미리 JS, CSS 등의 리소스를 로드할 수 있다.

<link rel="preload" href="/styles/main.css" as="style">
<link rel="preload" href="/scripts/utils.js" as="script">

 

 

 

#2. Tree Shaking

Tree Shaking은 코드에서 사용되지 않는 모듈을 제거하는 최적화 기법이다.

모듈 번들링 시, 실제로 사용되는 코드만 포함하여 번들 크기를 줄일 수 있다.

// 모든 array-utils 모듈을 가져오는 경우 (크기 증가)
import arrayUtils from 'array-utils';

// 필요한 함수만 가져오는 경우 (Tree Shaking 가능)
import { unique, implode, explode } from 'array-utils';

 

이는 ES6 모듈 시스템의 import/export 구조를 기반으로 작동하며, Webpack은 이 기능을 자동으로 수행한다.

 

 

 

 

Q. 자바스크립트 번들러가 꼭 자바스크립트(Node.js)로 만들어야 할까?

과거에는 번들링 툴들이 대부분 자바스크립트(Node.js)로 개발되었지만, Node.js의 성능 한계와 웹 브라우저의 ESM(ES6 모듈) 기본 지원 덕분에 더 빠르고 효율적인 네이티브 언어 기반의 번들러들이 등장하게 되었다.

 

 

🎯 새로운 네이티브 언어 기반 번들러

◼︎ esbuild (GO 기반)

GO 언어로 개발되어 빠른 트랜스파일링 성능을 제공한다.

멀티스레드 지원 덕분에 Node.js 기반 번들러보다 더 빠른 속도를 보여준다.

Vite, Snowpack 같은 툴에서도 esbuild를 래핑하여 사용하고 있다.

 

◼︎ turopack (Rust 기반)

Rust로 개발되어 빠른 빌드 속도를 자랑하며, Webpack의 단점을 개선하고 성능 최적화에 초점을 맞추었다.

Turbopack은 특히 개발 서버 환경에서 빠른 HMR 성능을 제공한다.

 

◼︎ SWC (Rust 기반)

번들링 작업은 turbopack으로 이관되었으며, SWC는 주로 React, Typescript 코드의 트랜스파일링에 사용된다.

Next.js에서도 SWC를 기본 컴파일러로 채택하여 성능을 높이고 있다.

 

 

 

🔥 Vite 이해하기: Turbopack 과의 비교

Vite는 최신 번들링 툴로 esbuild를 래핑하여 빠른 개발 환경을 제공한다.

Vite의 주요 특징은 Dependencies(외부 라이브러리)와 Source Code(어플리케이션 소스 코드)를 분리하여 번들링하는 것이다.

 

#1. Vite의 작동 방식

◼︎ Dependencies

esbuild를 사용하여 외부 라이브러리를 미리 트랜스파일링하고, ESM 방식으로 동적으로 로드한다.

이는 의존성 라이브러리들이 변경될 일이 적기 때문에, 빌드 시 트랜스파일링 과정을 한 번만 수행해 성능을 최적화한다.

 

◼︎ Source Code

entry point에 해당하는 소스 코드는 서버 요청시 즉시 반환된다.

Vite는 소스 코드 변경 시 빠른 HMR 지원한다.

 

 

#2. Webpack과의 차이점

Webpack은 번들링에 필요한 모든 설정을 개발자가 직접 설정해야 하는 반면, Vite는 이러한 기능들을 자동으로 제공해 초기 설정과 구성이 훨씬 간단하다. Turbopack은 Webpack의 후속작으로 Vite와 유사하게 빠른 빌드와 HMR을 제공한다. 특히 Rust 기반의 저수준 최적화 덕분에 개발 환경에서의 속도가 매우 뛰어나다.

  Vite Turbopack
기반 언어 Go Rust
성능 빠른 개발 성능, 초기 로드 최적화 더 빠른 빌드 속도, 최적화된 HMR
구성 자동 번들링 최적화 Webpack 후속으로 높은 호환성
사용 사례 Vite 기반 프로젝트, Vue/Nuxt.js Next.js 등 Webpack 대체용

 

반응형