작성
·
339
3
안녕하세요!
저는 리액트 컴포넌트 라이브러리를 도전해보고 있습니다. 대부분은 Rollup, Microbundle 등의 번들러를 활용한다는 것을 알게 되었습니다. 그런데 조사해보니 굳이 번들러 없이도 컴포넌트 라이브러리를 만들 수 있다는 것을 알게 되었습니다.
tsc 컴파일을 통해 TypeScript 환경과 JavaScript 환경 둘 다 지원하는 라이브러리를 만들었고, 샘플 프로젝트에서 설치해본 결과 잘 작동합니다.이런 상황에서 번들러가 왜 필요한지 잘 모르겠습니다.
번들러의 역할에 대해서는 알고 있습니다. 폴리필을 제공하기도 하고, 하나의 자바스크립트 파일로 만들어서 네트워크 요청 횟수를 줄여주기도 하지요.
하지만, 이 번들러를 컴포넌트 라이브러리에 꼭 사용해야 하는지 궁금합니다. 제가 만든 이 컴포넌트 라이브러리를 배포하고, 한 샘플 프로젝트에서 이 라이브러리를 npm install 했다고 가정해보겠습니다. 이 프로젝트는 Vite를 번들러로 사용하고 있습니다. 결국 배포할 때 번들링을 하게 될 텐데, 그러면 컴포넌트 라이브러리도 자동으로 함께 번들링에 포함되지 않나요? 어차피 프로젝트에서 번들링 될 건데, 미리 번들링할 필요가 있을까요?
답변 1
2
안녕하세요 좋은 질문 감사합니다!
1.
여러 파일을 하나로 합쳐주는 번들링은 말씀하신대로 꼭 필요하진 않아요. 엔드 유저가 vite 같은 번들러를 이미 이용하고 있다면 굳이 라이브러리에서 미리 번들링할 필요가 없죠. 하지만, 유저가 번들러를 사용하고 있지 않다면? 그렇다면 waterfall 네트워크 요청이 발생하겠죠. 요즘 번들러 없이 개발하는 상황이 얼마나 있을진 모르겠지만 그 최종 환경을 컨트롤 할 수 없는 라이브러리 제작자 입장에서 번들링을 굳이 피할 필요는 없는 것이죠.
2.
그런데 번들링하는 과정에서 rollup 같은 툴이 해주는 작업 중 하나가 treeshake 인데요. 만드신 라이브러리의 main.js 에서 a.js 라는 파일을 import 하고 있다고 가정하고...
// main.js
import { message } from './a.'
console.log(message)
// a.js
export const message = "Hello"
export const ver = "0.0.1"
이런 상황에서 ver 는 사실상 쓰이지 않고 있는데요. Rollup 같은 툴이 번들링 하는 과정에서 저렇게 쓰이지 않는 ver 를 bundle output 에서 제외 시켜주고 이를 treeshake 이라 부릅니다.
번들러 없이 그냥 raw 하게 tsc 로만 transpile 된 파일들을 통째로 내보내게 되면, 이를 import 하는 유저에게도 같은 불필요한 비용이 전달되겠죠.
3.
그리고 어떤 라이브러리냐에 따라 다르지만 예를 들어 리액트 UI 컴포넌트 라이브러리를 만드시게 된다면 .jsx 나 .tsx 를 작성하시게 될테고 이를 .js 로 transpile 하는 과정이 필요한데요. 이 또한 직접 필요한 툴 + tsc 조합으로 하실 수 있지만, 아무래도 rollup 이나 그에 기반하는 vite 툴들에 플러그인이 잘 되어 있어 손쉽게 transpile 할 수 있어요.
4.
그래서 rollup 같은 툴을 쓰는 이유는 혹시 모를 상황을 위해 번들링을 해주는 것 뿐만 아니라 다양한 transpile 작업을 간편하게 설정하려는 의도도 큽니다. 게다가 라이브러리 개발하는 도중에 HMR 등 모던한 개발 환경을 구축하는데에도 도움이 되지요.
또 이해 안가는 점 있으시면 질문 주세요. 디테일하고 예리한 질문 덕에 자세한 설명 적어볼 수 있었습니다 😊
아 이게 헷갈릴 수 있는데요. 라이브러리 제작하는 입장에서 treeshake 를 안한 버전을 제공했을 때,
유저가 번들러를 사용해서 treeshake 하게 되면, 결과적으로는 treeshake 된 버전이 배포되는 거니까 상관이 없어요. 그런데 (또 반복되는 내용이지만) 유저가 treeshake 기능을 갖춘 번들러를 사용하느냐... 라고 했을 때 그게 100% 아니라는 점? 그래서 라이브러리 제작자는 엔드유저가 어떤 상황에서 사용할지 모르니까 최대한 정리한 버전을 배포하는 관행 정도라고 생각하시면 돼요. 그런데 이것도, 불필요한 polyfill 을 포함한 결과를 내보낸다던가 하면 결국엔 유저 입장에서 불필요한 오버헤드가 되기도 해서, 라이브러리 제작자가 어디까지 작업을 해서 배포를 해야 하느냐에 대해서는 지속적인 토론과 변화가 있어 오고 있는 상태라 정확한 정답은 없습니다.
그리고 실험하신 내용에서 A1 을 만드실 때 react, react-dom 같은 dependencies 가 포함된 것 같아보이는데요. 리액트 컴포넌트 라이브러리를 쓰는 프로젝트라면 리액트는 아마 당연하게도 이미 갖고 있을테니까, 보통 이런 경우에는 rollup 같은 툴에서는 external한 값으로 그 dependencies 들을 번들 결과에서 제외 시킵니다.
설명 감사합니다! 덕분에 번들러에 대해 더 깊게 이해할 수 있었어요.
그런데 계속 머릿속에 멤돌고 있는 가설이 하나 있습니다.
"라이브러리를 번들링하면 하나의 자바스크립트 파일이 나오게 됨. 이 라이브러리를 사용하는 컨슈머 프로젝트에서는, 이 자바스크립트 파일 하나를 통째로 import하게 되니까 트리쉐이킹할 수가 없어서, 오히려 사이즈가 더 커짐. 그러니까 차라리 번들링 없이 제공하고, 번들링은 컨슈머 프로젝트에서 진행."
이 가설이 맞을까요?
제 나름대로 테스트해본 결과, 이 가설은 거짓으로 밝혀졌으나 조건을 명확하게 설정한 실험인지는 모르겠습니다.
A라는 리액트 컴포넌트 라이브러리를 번들러로 빌드한 경우와(A1) tsc만 사용한경우 (A2)로 나누어보았습니다. A1의 경우 빌드 아웃풋이 60KB가 나온반면, A2의 경우 빌드 아웃풋이 2.03KB로 측정되었습니다. (단순히 폴더 우클릭하고 상세정보에 나온 사이즈입니다)
A1은 리액트, 리액트돔 등의 라이브러리도 짬뽕되어 하나의 아웃풋이 나왔으니 당연히 파일크기가 클것이며, 이 A1을 사용하는 컨슈머 프로젝트의 입장에서는, 오히려 하나의 거대한 덩어리를 import 해오니까 비효율 적이지 않을까? 라고 생각했습니다. 트리쉐이킹이 불가능하다고 생각했습니다.
하지만 컨슈머 프로젝트를 빌드해본 결과, 오히려 A2를 사용했을 때의 최종 빌드사이즈가 더욱 컸습니다. 도대체 어쩌다가 이런 결과가 나타나게 된 걸까요?
제 실험이 잘못되었다면, 마음에 걸리는 부분은 이 명제입니다.
"번들러로 하나의 js 파일을 만들어 내면, import 해 왔을 때 트리쉐이킹이 불가능하다" 입니다.
번들러 너무 어렵습니다. 도와주세요!