Rollup으로 JavaScript 번들링 시작하기

Rollup으로 JavaScript 번들링 시작하기

ViteWebpack 같은 도구는 많이 들어 봤어도 Rollup은 조금 생소할 수 있는데요. 사실 우리가 매일 쓰는 수많은 도구가 그 밑에서 Rollup을 돌리고 있습니다. Vite는 한동안 프로덕션 빌드에 Rollup을 그대로 썼고, React나 Vue 같은 라이브러리도 Rollup으로 번들링되어 배포됩니다. 그러니까 Rollup은 화려하게 드러나진 않지만 JavaScript 생태계의 바닥을 받치고 있는 도구인 셈이죠. 🛠️

이번 글에서는 Rollup이 정확히 무엇이고 왜 만들어졌는지부터 짚어 보고, 직접 번들을 만들어 보면서 트리 셰이킹과 여러 포맷 출력, 플러그인 활용법까지 하나씩 살펴보겠습니다.

Rollup이란 무엇일까요?

Rollup은 여러 개의 JavaScript 파일을 하나로 묶어 주는 모듈 번들러입니다. 우리가 코드를 작성할 때는 기능별로 파일을 잘게 나누는 게 관리하기 편하지만, 그 상태 그대로 배포하면 파일이 너무 많아 비효율적인데요. 번들러는 이렇게 흩어진 import/export로 연결된 파일들을 따라가며 하나의 결과물로 합쳐 줍니다.

Rollup이 특별한 이유는 처음부터 ES 모듈(ESM)을 기준으로 설계되었다는 점입니다. importexport 구문을 1급 시민으로 다루기 때문에, 모듈 사이의 의존 관계를 정적으로 분석하는 데 강합니다. 이 특성 덕분에 사용하지 않는 코드를 걸러내는 트리 셰이킹이 자연스럽게 나오고, 군더더기 없이 깔끔한 번들을 만들어 냅니다. 그래서 Rollup은 특히 라이브러리를 만들어 배포할 때 가장 많이 선택됩니다.

왜 Rollup을 쓸까요?

번들러는 Webpack, esbuild, Parcel 등 여러 가지가 있는데 Rollup이 자리를 지키는 이유가 있습니다.

우선 출력물이 깔끔합니다. Webpack이 만드는 번들은 모듈을 함수로 감싸고 자체 로더 코드를 덧붙이는 반면, Rollup은 원본 코드에 가깝게 평평한 결과물을 뽑아냅니다. 뒤에서 직접 보겠지만, 우리가 작성한 코드와 번들 결과물이 거의 똑같아서 디버깅하기도 좋습니다.

또한 하나의 소스로 여러 모듈 형식을 동시에 뽑을 수 있습니다. 라이브러리를 배포하려면 ESM을 쓰는 환경과 CommonJS를 쓰는 환경을 모두 지원해야 하는데, Rollup은 설정 하나로 두 포맷을 한 번에 만들어 줍니다.

마지막으로 Rollup은 Vite의 기반이기도 합니다. Vite는 개발 서버에서는 esbuild를, 프로덕션 빌드에서는 오랫동안 Rollup을 사용해 왔습니다. 그래서 Rollup의 플러그인 생태계와 설정 방식을 알아 두면 Vite를 다룰 때도 그대로 도움이 됩니다.

그럼 실제로 번들을 만들어 보겠습니다. 먼저 프로젝트를 준비합니다.

mkdir rollup-demo && cd rollup-demo
bun add -d rollup

첫 번들 만들기

간단한 모듈 두 개를 만들어 보겠습니다. 하나는 함수를 정의하고, 다른 하나는 그 함수를 가져다 씁니다.

src/math.js
export function add(a, b) {
  return a + b;
}

// 이 함수는 어디에서도 import하지 않습니다
export function subtract(a, b) {
  return a - b;
}
src/main.js
import { add } from "./math.js";

console.log("1 + 2 =", add(1, 2));

이제 명령어 한 줄로 번들을 만들 수 있습니다. --file로 출력 파일을, --format으로 모듈 형식을 지정합니다.

bunx rollup src/main.js --file dist/bundle.js --format esm

생성된 dist/bundle.js를 열어 보면 흥미로운 점이 보입니다.

dist/bundle.js
function add(a, b) {
  return a + b;
}

console.log("1 + 2 =", add(1, 2));

두 파일이 하나로 합쳐졌는데, add 함수만 들어 있고 subtract는 사라졌습니다.

트리 셰이킹이 일어나는 과정

방금 subtract가 번들에서 빠진 것이 바로 트리 셰이킹(tree shaking)입니다. 나무를 흔들면 죽은 잎이 떨어지듯, 실제로 사용되지 않는 코드를 번들에서 털어내는 최적화인데요.

Rollup은 main.jsaddimport하는 것을 보고, subtract는 아무도 쓰지 않는다고 판단해 결과물에서 제외합니다. 이런 분석이 가능한 이유가 앞서 말한 ESM 설계입니다. import/export는 코드를 실행해 보지 않고도 정적으로 분석할 수 있어서, 어떤 함수가 실제로 쓰이는지를 빌드 시점에 정확히 파악할 수 있습니다.

라이브러리 입장에서 트리 셰이킹이 잘 되는 번들을 제공하면, 그 라이브러리를 쓰는 쪽에서도 필요한 함수만 가져다 쓰고 나머지는 자기 번들에서 떨궈낼 수 있습니다. 번들 크기가 곧 사용자 경험으로 이어지는 프론트엔드에서 이건 꽤 중요한 장점입니다.

여러 포맷으로 한 번에 출력하기

명령어로 옵션을 일일이 넘기는 건 번거로우니, 보통은 rollup.config.js 설정 파일을 만듭니다. 설정 파일을 쓰면 한 번의 빌드로 여러 포맷을 동시에 뽑아낼 수 있는데요. ESM, CommonJS, 그리고 압축된 버전까지 한꺼번에 만들어 보겠습니다.

설정 파일에서 ESM 문법을 쓰려면 package.json"type": "module"을 추가해야 합니다.

package.json
{
  "name": "rollup-demo",
  "type": "module"
}
rollup.config.js
export default {
  input: "src/main.js",
  output: [
    { file: "dist/bundle.js", format: "esm" }, // 모던 환경용
    { file: "dist/bundle.cjs", format: "cjs" }, // Node.js require용
  ],
};

output을 배열로 주면 각 항목마다 다른 포맷의 결과물이 만들어집니다. -c(또는 --config) 옵션으로 설정 파일을 사용해 빌드합니다.

bunx rollup -c
결과
src/main.js dist/bundle.js, dist/bundle.cjs...
created dist/bundle.js, dist/bundle.cjs in 58ms

CommonJS로 출력한 dist/bundle.cjs를 보면 ESM 버전과 달리 'use strict'가 붙고 Node.js 환경에 맞는 형태로 변환되어 있습니다.

dist/bundle.cjs
"use strict";

function add(a, b) {
  return a + b;
}

console.log("1 + 2 =", add(1, 2));

같은 소스 코드 하나로 환경별 결과물을 동시에 얻은 것이죠.

플러그인으로 기능 확장하기

Rollup의 핵심 기능은 모듈을 묶는 것까지입니다. 코드 압축, 외부 패키지 해석, TypeScript 변환 같은 추가 작업은 플러그인이 담당합니다. 필요한 기능만 골라 끼우는 방식이라 번들러 자체는 가볍게 유지됩니다.

대표적으로 코드를 압축해 주는 @rollup/plugin-terser를 추가해보겠습니다.

bun add -d @rollup/plugin-terser
rollup.config.js
import terser from "@rollup/plugin-terser";

export default {
  input: "src/main.js",
  output: [
    { file: "dist/bundle.js", format: "esm" },
    // 이 출력에만 terser 플러그인을 적용해 압축합니다
    { file: "dist/bundle.min.js", format: "esm", plugins: [terser()] },
  ],
};

플러그인은 전체 빌드에 적용할 수도 있고, 위처럼 특정 output에만 적용할 수도 있습니다. 빌드 후 압축된 결과물을 보면 공백과 줄바꿈이 모두 사라진 것은 물론, 똑똑하게 연산까지 미리 계산해 둔 것을 볼 수 있습니다.

dist/bundle.min.js
console.log("1 + 2 =", 1 + 2);

이 밖에도 node_modules의 패키지를 찾아 번들에 포함시키는 @rollup/plugin-node-resolve, CommonJS 모듈을 ESM으로 바꿔 주는 @rollup/plugin-commonjs 등이 자주 함께 쓰입니다. 플러그인이 정확히 어떻게 동작하는지, 직접 만드는 방법이 궁금하다면 Vite 플러그인 만들기를 참고하세요. Vite와 Rollup은 플러그인 인터페이스를 공유하기 때문에 거의 그대로 적용됩니다.

Webpack과는 무엇이 다를까요?

비슷한 도구인 Webpack과 자주 비교되는데, 둘은 지향점이 조금 다릅니다. Webpack은 코드 스플리팅, 핫 모듈 교체, 다양한 에셋 로더 등 애플리케이션을 만드는 데 필요한 기능을 폭넓게 갖춘 종합 번들러입니다. 반면 Rollup은 모듈을 깔끔하게 묶는 일에 집중하며, 군더더기 없는 결과물을 중시합니다.

그래서 보통 복잡한 웹 애플리케이션은 Webpack(이나 Vite)으로, 재사용할 라이브러리는 Rollup으로 번들링하는 경우가 많았습니다. 물론 요즘은 Vite가 두 영역을 모두 흡수하면서 이 경계도 점점 흐려지고 있습니다.

마치며

지금까지 Rollup이 무엇이고 왜 라이브러리 번들링에 강한지부터, 트리 셰이킹과 여러 포맷 출력, 플러그인 활용까지 직접 해보았습니다. Rollup의 핵심은 ESM을 기준으로 모듈을 깔끔하게 묶고, 필요한 기능만 플러그인으로 더하는 단순한 철학에 있습니다.

여기서 한 걸음 더 나아가고 싶다면, 직접 만든 라이브러리를 npm에 배포하면서 ESM과 CommonJS, 그리고 타입 선언 파일까지 함께 번들링하는 방법을 살펴보면 좋습니다. 애플리케이션 개발이라면 Rollup을 기반으로 개발 서버와 빠른 HMR까지 얹은 Vite 관련 글도 함께 살펴보세요.

더 자세한 설정 옵션과 플러그인 목록은 Rollup 공식 문서에서 확인할 수 있습니다.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord