🎁

[JS] Bundling (번들링)


번들링의 탄생 배경

번들링은 모듈화된 소스 코드를 브라우저에서 실행할 수 있는 파일로 한데 묶어주는 작업이다.

왜 번들링이 필요할까?

ES modules이 2015년에 도입되기 전에는 JavaScript 모듈화를 네이티브에서 진행할 수 없었다. 모듈에 관한 자세한 내용은 여기서 다룬다.

모둘 시스템이 없을 때는 HTML 파일에 스크립트 파일을 불러와서 사용할 경우 전역 공간에 모든 변수가 노출될 수 있었다. 여러 스크립트를 불러왔때 변수의 이름이 겹쳐 충돌이 발생하는 문제점이 생겼다.

1<html>
2 <script src="/src/foo.js"></script>
3 <script src="/src/bar.js"></script>
4 <script src="/src/baz.js"></script>
5</html>

foo.js, bar.js, baz.js 중 하나라도 변수 이름이 겹치는 선언이 전역에 있다면 충돌이 발생한다.

2008년에 구글에서 브라우저 외부에서도 JavaScript를 실행시킬 수 있는 V8엔진을 소개하면서 모듈화에 대한 필요성이 더욱 부각됐다. 그리고 2009년을 기점으로 모듈 표준화를 위한 움직임이 본격적으로 시작됐다.

개발자들은 모듈화를 위해 CommonJS나 AMD(Asynchronous Module Definition)같은 모듈 시스템을 직접 디자인했지만, 이는 표준이 아니기 때문에 브라우저에서 지원하지 않았다. 특히 'Node.js'에서 채택하게 되면서 폭발적인 성장을 한 CommonJS의 경우 동기적으로 모듈을 호출하는 방식을 선택했기 때문에 브라우저에서는 사용할 수 없었다.

이후 자바스크립트 자체에서 모듈을 지원해야 한다는 필요성이 높아졌고, 2015년에 ES6의 등장과 함께 ES Module이 탄생했다. 동기/비동기 로드를 지원, 편리한 순환 참조 관리, 쉬운 트리 쉐이킹을 가능하게 했지만, IE(항상 이 녀석이 문제다) 같은 구형 브라우저에서는 제대로 동작하지 않는다는 문제점이 있었다.

아직 모든 브라우저에서 모듈 시스템을 완전하게 지원하지 않았기 때문에 개발자들은 '번들링'이라는 우회적인 방법을 사용해야 했다. 따라서 브라우저와 무관하게 모듈 시스템을 지원하기 위해 Webpack, Rollup 그리고 Parcel 같은 번들링 도구, 번들러가 탄생했다.

모듈 번들러

모듈 번들러란 웹 애플리케이션을 구성하는 자원(HTML, CS, 자바스크립트 등)을 모두 각각의 모듈로 보고, 이를 조합해 병합 및 압축된 하나의 결과물을 만드는 도구다. 기존의 모듈은 스크립트에 국한됐지만, 번들러는 웹 애플리케이션을 구성하는 모든 자원을 모듈로 지칭한다. HTML, CSS, JavaScript는 물론 이미지나 폰트도 포함된다.

Webpack, Rollup, Parcel 같은 기존 번들러들은 엔트리 JavaScript 파일부터 시작해서 소스 코드와 node_modules 폴더 전체 코드 베이스를 묶고, 필드 프로세스를 통해 실행한 다음 번들된 코드를 브라우저에 제공한다. 이 말을 바꿔 말하면 애플리케이션의 모든 소스 코드에 대해 크롤링 및 빌드 작업을 마쳐야만 실제 페이지를 제공할 수 있다는 의미다.

때문에 웹 애플리케이션이 커지면 커질수록 애플리케이션을 시작하는데 오래 걸리게 된다.

또한 대부분의 대부분의 번들러는 Node.js 기반으로 돌아가기 때문에 싱글 스레드로 인한 처리 한계를 가진다.

차세대 모듈 번들러

위에서 나타난 문제점을 해결하기 위해 snowpack, vite 같은 차세대 번들러들이 등장했다.

어떤 점을 기존에 비해 개선했는지 알아보자.

더 빠른 서버 구동

번들러 기반의 도구는 (캐싱한 데이터가 없다면) 애플리케이션 내 모든 소스 코드에 대해 크롤링 및 빌드 작업을 마쳐야 실제 페이지를 제공할 수 있다고 했다. 기존 번들러는 Node.js 기반으로 돌아가기 때문에 컴포넌트 라이브러리/프레임워크와 같이 몇 백개의 모듈을 갖고 있는 매우 큰 디펜던시에 대한 번들링 과정이 비효율적이었고 많은 시간을 소요했다.

이를 극복하기 위해 차세대 번들러는 이 동작을 네이티브 영역에서 돌려 성능을 높였다. Golang을 기반으로하는 'Esbuild', Rust를 기반으로 하는 'SWC'가 대표적이다.

Snowpack와 Vite에서 esbuild를 사용한 사전 번들링을 통해 기존 번들러 대비 10-100배 빠른 속도를 가진다.

더 빠른 소스 코드 갱신

기존 번들러는 파일 하나가 변경되면 연관된 모든 파일을 다시 빌드하고 다시 번들링 해야해 비효율적이었다.

차세대 번들러는 Native ESM을 이용해 문제점을 개선했다. 소스코드는 Native ESM을 사용하고, NPM 패키지들은 Esbuild로 사전 번들링을 수행한다.

엥? 이게 무슨 소릴까? 아래 이미지를 보자.

위에 초록색 상자로 표시된 route를 어떤 페이지라고 해보자. 이 페이지를 요청하면 브라우저는 route에 해당하는 브랜치의 정보만 필요하다. 따라서 브라우저는 ESM을 통해 이 페이지의 모듈에 대한 소스 코드만을 번들러에게 요청하고, 번들러는 해당하는 소스 코드만을 전달한다.

어떤 모듈이 수정됐을 때도 번들러는 수정된 모듈과 관련된 부분만 교체하고, 브라우저에서 해당 모듈을 요청하면 교체된 모듈을 전달하면 되기 때문에 전체를 다시 번들할 필요가 없다. 기존 번들러도 HMR(Hot Module Replacement)을 지원하기 시작했지만 앱 사이즈가 커질수록 선형적으로 갱신에 필요한 시간이 증가했다.

여기서 더 나아가 vite의 경우 HTTP 헤더를 이용해 요청한 소스 코드가 304 Not Modified, 디펜던시는 Cache-Control: max-age=31536000,immutable를 이용해 캐시되도록 함으로써 요청을 줄여 성능을 더 높였다.

마치며

번들러를 사용해보고 싶어 뭘 사용할까 살펴보다 'Vite'에 대해 알게됐다.

적용해보기 전에 이전 번들러들과 뭐가 다른지 알아봤다. 특히 가장 많이 사용하고 유명한 webpack과의 비교를 집중적으로 검색했었다.

Webpack을 실제 프로젝트에 처음부터 적용해본 적은 없지만, webpack과 비교해 엄청 간단한데다 성능도 좋다는 점에서 정말 매력적인 것 같다. 솔직히 webpack은 문서만 봐도 너무 복잡해서 사용해보고 싶은 마음이 전혀 생기지 않았었다ㅋㅋ.

실제 프로젝트에도 적용해봤는데 딱히 문제 없이 빠르고 편하게 적용돼서 놀랐다. 앞으로 다양한 프로젝트에 적용해봐야겠다.


참조:
Vite
https://ui.toast.com/weekly-pick/ko_20220127
https://radixweb.com/blog/webpack-vs-vitejs-comparison
https://engineering.ab180.co/stories/webpack-to-vite
https://www.youtube.com/watch?v=nBYo2B2XN5s
https://wormwlrm.github.io/2020/08/12/History-of-JavaScript-Modules-and-Bundlers.html
https://black7375.tistory.com/82