2024.08.10 - [Development Environment] - Why Vite? JS 모듈화의 역사, CJS, ESM, Webpack
얼마 전, Vite를 써야 하는 이유에 대한 글을 쓰면서 Rollup 라이브러리를 접하고 JavaScript Module의 역사, 번들링에 대한 개념을 알게 되었습니다. 그 글을 쓰고 나서 ES Modules와 CommonJS의 차이점을 Tree-shaking 관점으로 집중하여 또 포스팅하고 싶어 졌습니다. (차주 목요일에 발표라 끝나고 포스팅할 것 같습니다.)
제가 원래 영화든 책이든 이런 식으로 디깅을 좋아하는데요...(tmi) 그래야 개념이 확장되고 기억도 잘나고.. 머릿속에 시각화 되는 걸 즐기는 듯합니다.
여하튼 공부 중 제 눈을 사로잡은 글이 있었습니다. Rollup을 만든 Rich Harris가 2016년 Medium에 쓴 글이었어요. 해당 글을 통해 Left-pad 사건을 접하게 되었는데 번들링과 연관 지어서 영어 공부도 할 겸 간략하게 소개해보고 싶었습니다.
Left-pad 사건과 Rich Harris의 글을 통해 번들링의 중요성을 알아보겠습니다.
On March 22, 2016...npm left-pad incident
소프트웨어 엔지니어 Azer Koçulu는 npm에 273개 가량의 패키지를 퍼블리싱하고 있었는데, 그중 'kik'이라는 패키지가 있었다. 어느 날 'Kik'이라는 회사와 상표권 분쟁을 하며 회사에서 해당 패키지의 이름을 바꿔달라고 굉장히 무례하게 요청을 했다. Azer는 거절했고 협상이 결렬되자 Kik은 npm에게 중재를 요청한다. 이후 npm이 kik 패키지명에 대한 소유권을 Kik에게 넘겨주게 된다. 분노한 Azer는 npm의 행동에 항의하고자 자신이 그동안 npm에 공개한 모든 패키지를 삭제한다.
이 일은 나비효과처럼 굉장한 파급력을 불러오게 되는데...
삭제된 패키지중 `left-pad` 패키지는 수천 개의 다른 소프트웨어에서 의존성으로 사용되었고 삭제 전까지 1,500만 회 이상 다운로드 되었었다. 이 중 JS 생태계에 있어 매우 중요한 Babel(이전 버전과 호환이 되게 해주는 트랜스 컴파일러), Webpack(모듈 번들링 시스템), React(메타에서 개발한 웹 라이브러리), React Native(메타가 개발한 앱 라이브러리)가 포함되어 있었다. 이를 이용하는 Meta, Paypal, Netflix, Spotify 등과 같은 여러 테크 기업들도 타격을 입게 된다. JS 프로젝트를 빌드하거나 설치하려는 사용자는 404 error를 받으며 실패하게 된다. 재밌게도 Kik 회사의 개발자들도 해당 패키지 삭제로 빌드 문제에 직면하게 된다.
Azer는 Medium에 '내 모듈들을 해방시켰다'는 제목으로, 모든 소프트웨어 프로젝트를 기업의 이익수단으로 여기는 것에 대한 항의의 일환으로 삭제했다고 포스팅했다. 다른 개발자가 left-pad 패키지를 재구성했지만 1.0.0 버전으로 출시했고 Azer의 패키지는 0.0.3버전이었기에 여전히 문제를 겪었다.
삭제된 지 약 두 시간 후, npm은 백업을 복원하여 원래의 0.0.3 버전을 수동으로 다시 게시함으로써 문제를 해결했다. 또한 릴리즈 된 지 24시간이 지난 패키지거나 의존성이 있는 경우 삭제를 방지하는 정책을 발표했다. Babel을 포함한 오픈 소스 프로젝트 개발자들은 의존성을 제거하기 위해 이 사건으로 인해 긴급 업데이트를 했다.
이렇게 2시간가량 인터넷을 헤집어 놓은 패키지는 단 11줄 밖에 되지 않는다.
module.exports = leftpad;
function leftpad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = '';
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}
exports를 통해 commonJs를 사용하는 것을 알 수 있다. 원본 문자열 str과 최종적으로 만들고 싶은 문자열 길이 len과 str의 길이를 맞출 문자 ch를 매개변수로 받는다. 여기서 ch가 주어지지 않을 경우 '' 빈 문자열로 채운다. 원하는 길이와 원본 문자열의 값 차이만큼 채운다. 음수거나 0이면 패스된다.
문자열을 특정 길이에 맞추기 위해 앞에 패딩을 넣는 패키지인 것인데, 고정된 자릿수가 필요하거나 정렬된 문자열로 출력을 하고 싶거나 할 때 사용되었다고 한다.
Javascript 생태계의 특징, 작은 기능들이 모듈화 된 소프트웨어의 구조를 실감하게 된 사건으로 느껴진다. 이 사건은 굉장히 파장이 컸던 만큼 개발자들의 각기 다른 의견과 감정적인 말들이 오고 갔는데, 그 중 Rollup의 창시자 Rich Harris의 글을 같이 보았으면 한다. 그의 글은 left-pad의 사건을 요약해주고 해당 사건을 방지할 수 있는 방법까지 제시한다. 아래부터 해리스의 글이다. 👀
한 가지 기이한 속임수로 인터넷을 망가뜨리지 않는 방법
Javascript 도구나 라이브러리를 작성할 때 당신은 코드를 게시하기 전에 번들링을 해야 합니다.
몇 시간 전, Azer는 상표권 분쟁으로 인해 npm에서 그의 컬렉션을 해방시켰습니다. 그중, 문자열 앞에 0을 넣는 11줄 유틸리티(left-pad)를 전체 인터넷 세상에서 크게 의존하고 있는 Babel을 포함한 다른 모듈들도 크게 의존하고 있었습니다.
그렇게 인터넷은 망가져버렸습니다.
(원본의 사람들의 분노와 당혹함에 대한 견해 캡쳐들이 더 있으나 생략합니다.)
이 일과 관련된 모든 분들께 동정심을 느낍니다. 모든 사람에게 고통스럽고, 그리고 특히 Azer(여러분한테 빚진 게 하나도 없다)에게 더 그렇습니다. Github 스레드를 보면 이 문제가 아주 쉽게 해결될 수 있다는 사실에 짜증 날 것입니다.
브라우저용을 위해서가 아니더라도 코드를 번들링을 하세요.
1. left-pad는 게시가 취소되었습니다.
2. Babel은 특정 버전에 의존성을 가지고 있는데 그중 하나가 left-pad였습니다.
3. Babel을 설치할 때 Babel이 의존하는 모든 패키지들도 함께 설치됩니다. 이 패키지들의 의존성까지 포함해서요.
4. left-pad가 삭제되면서, 과거의 Babel 버전들은 더 이상 제대로 설치될 수 없었습니다.(즉, 망가졌어요). left-pad가 복원될 때까지요.
5. 이로 인해 많은 사람들이 사건의 원인으로 left-pad의 개발자 Azer를 비난하게 되었습니다.
여기서 핵심은 3번입니다. package.json에 모든 의존성을 나열하는 대신 모든 의존성을 일렬로 나열하여 Bundle 형태로 배포했다고 가정해 봅시다. (left-pad 사태의 가장 큰 희생자이기 때문에 Babel을 언급할 뿐, 그는 규범을 준수하고 있습니다. - 제가 주장하고자 하는 것은 이 규범이 실제로 말이 안 된다는 점입니다.)
제가 말한 방식이었다면, 구 버전 Babel은 잘 작동했을 것이며 관리자는 다음 버전이 나오기 전에 대체할 시간을 가질 수 있었을 겁니다.
모든 것을 명확히 하자면, 저는 Rollup의 창시자로서 당연히 Code-Bundling을 해야 한다고 생각합니다. 하지만 이 글의 목적상 Rollup, Webpack, Browserfy 혹은 무언가를 사용하든 그것은 중요하지 않습니다. - 중요한 것은 아이디어지, 구체적인 도구가 아닙니다.
우리가 경험한 혼돈을 피할 수 있을 뿐만 아니라 상당히 많은 이점이 있는 것으로 밝혀졌습니다.
설치 시 걸리는 시간이 아주 짧아집니다.
연결이 좋지 않은 상태에서 npm3를 통해 많은 의존성이 있는 패키지를 설치해보셨나요? 의존성의 의존성에 대한 네트워크 요청이 폭발적으로 증가하기 때문에 끔찍한 경험이 될 겁니다. 게시자에 따라 필요하지 않은 부분까지 다운로드할 수 있어요. 의존성이 없을 경우 단일 tar파일을 다운로드하여 완료할 수 있습니다.
비과학적인 테스트로 대략 비슷한 기능을 하는 세 패키지, Rollup, Webpack, Browserfy를 예로 각각을 새로 설치하는 데 시간이 얼마나 걸릴까요?
47개의 직접적인 의존성을 가진 Browserfy는 (물론 많은 의존성이 자체적으로 또 다른 의존성을 갖고 있음) 26.4초가 걸렸습니다. 15개의 의존성을 가진 Webpack은 거의 30초가 걸렸습니다. Rollup은 3개(모두 내장된 CLI를 위한 것입니다. 이마저도 제거할 수 있습니다.)로 5초도 걸리지 않았습니다. 이는 개발자에게 훨씬 더 좋은 경험을 제공합니다.
Rollup도 의존성을 갖고 있지만 저희는 그 의존성들을 별도로 다운로드하게 하는 대신, 배포하기 전에 사전 번들링을 합니다. (네, Rollup은 Rollup을 번들링 합니다.)
Vite도 이 방식을..! ES Build로 사전 번들링, Rollup으로 배포시에 번들링!
디스크 공간을 덜 낭비합니다.
앞서 말씀드린 것에 대한 결과로, 수백 개의 전이되는 의존성들을 다운로드하지 않으면, 당연히 디스크에서 차지하는 공간도 적어집니다. npm3를 사용할 때 Browserfy는 2,459개의 항목으로 12.2MB를 차지합니다. Webpack은 3,093개의 항목으로 21MB를 차지합니다! Rollup은 209개의 항목으로 2.5MB만 차지합니다. (물론 이들이 직접적으로 비교될 수 없다는 것을 알고 있습니다. 하지만 제가 말하려는 요지는 이해하셨을 겁니다.)
시작이 더 빨라졌습니다.
Node의 'require'는 매우 느리다고 잘 알려져 있습니다. (Harris가 require의 속도를 지적할 수 있는 이유는 Rollup은 ES Modules를 사용하죠) 또다시 Babel을 예로 들겠습니다. - Babel 6이 출시 됐을 때, 수많은 작은 모듈들로 재구성되었기 때문에 시작하는 데 약 3초가 걸려서 전혀 쓸 수 없다고 느꼈습니다. 빠른 빌드에 익숙해진 상태에서 이건 정말 실망스러운 일이었습니다. npm3로 업그레이드하면서 개선되었는데 이는 플랫한 node_modules 구조 덕분이었습니다. 하지만 이 또한 아쉬운 점이, npm2가 훨씬 빨랐다는 점입니다.
당신의 라이브러리가 더 안정적입니다.
semver(유의적 버전 명세, Semantic Versioning Specification)를 사용하는 의존성이 있다면, 그 의존성에 대한 변경 사항이 당신의 라이브러리 사용자들에게 전달될 것입니다. 이론적으로는 좋은 일입니다. - 당신이 아무것도 하지 않아도 사용자들이 버그 수정을 받게 되니까요! - 하지만 실제로 그만큼 문제가 발생할 가능성도 높습니다. (semver가 그런 종류의 문제를 막아줄 것이라 생각한다면 아마도 당신은 이 일을 오래 하지 않았을 것입니다.)
위에서 말했듯 left-pad를 동일한 코드지만 1.0.0으로 배포해서 문제가 전혀 해결되지 못한 점이 이 케이스라고 할 수 있네요.
빌런들이 빌런짓을 하기 어렵게 만듭니다.
마찬가지로, 사용자가 당신의 라이브러리를 다운로드할 때 의존성도 함께 다운로드하게 된다면 npm에서 누군가가 그 의존성을 장악해 악의적인 행동을 하는 것에 대해 보호받지 못합니다. 이는 실제로 존재하는 위험입니다.
믿을만한 정보에 따르면, 많은 금융 기관들이 이런 이유로 Node와 npm을 사용하는 것을 거부합니다. - npm을 통해 악성 코드를 퍼뜨리는 것이 너무 쉽기 때문입니다. 번들링 된 코드를 배포하면 이런 종류의 공격에 노출되는 범위를 줄일 수 있습니다.
사용자에게 번들링을 강요하지 않습니다.
당신의 라이브러리가 브라우저에서 사용하기 적합하다면, 누군가가 언젠가는 그것을 번들링해야 할 가능성이 큽니다. 그런데 그때 문제가 발생할 수 있습니다. 올바른 도구와 설정을 해야 한 예상치 못한 문제가 발생하지 않기 때문입니다. PouchDB팀은 자신들의 라이브러리 사용자들이 Webpack을 사용하는 데 어려움을 겪고 있다는 걸 발견했습니다. 그 라이브러리는 Browserfy를 염두에 두고 만들어졌기 때문이죠. 결국 그들은 깨달음을 얻고, 하나의 거대한 index.js 파일을 제공하기 시작했습니다.
사용자들이 당신의 소스 코드를 번들링 하도록 기대하는 것은 ES6/TypeScript/CoffeeScript.. 등의 코드를 컴파일하라고 기대하는 것과 다를 바 없습니다. 이건 무례한 일입니다. 누군가에게 날 것의 재료를 주는 것과 요리된 음식을 주는 것의 차이입니다. - 만약 당신이 식당에 가서 햄버거를 주문했는데, 반 파운드의 다진 고기와 프라이팬을 받았다면 매우 화날 것입니다.
그래, 이해하지만 너의 말은 틀렸어!
이제 여러분 중 일부는 제가 분명 요점을 놓쳤다고 생각할 겁니다. 아마 다음과 같은 이유로요.
1. 번들링은 중복 제거를 방해합니다. 만약 내 의존성 두 개(foo와 bar)가 세 번째 의존성(baz)을 공유한다면, baz가 내 코드에 두 번 나타나는 것을 원하지 않습니다.
2. 으아아아아악 빌드 과정이 싫어요.
당신은 아마 제게 두 번째 의견을 이해해 줄 동정심이 없다는 것을 알아챌 수 있겠죠. 당신의 입장은 이해하지만 지금은 2016년이고 이런 식으로 일하는 데에는 이유가 있습니다. Keith Cirkel의 npm run 스크립트 사용 방법에 대한 글을 읽어보고, 여전히 빌드 과정이 지나치게 번거롭다고 생각하는지 확인해 보세요.
첫 번째 반대 의견은 설득력이 좀 더 있습니다. 이건 자신의 코드를 반드시 번들링 해야 한다는 주장에 대한 반대가 아닙니다. (예: lib/ 또는 src/에 여러 파일이 있는 경우, 이를 번들링해야 합니다). 자신의 코드뿐만 아니라 의존성까지 번들링해야 한다는 주장에 대해서는 일리가 있습니다. 하지만 이건 브라우저에만 해당됩니다. Node 환경에서는 일부가 중복되어도 신경 쓰지 않으며, 여러분도 그래야 합니다. - 디스크 공간은 저렴하고, 정신 건강은 비쌉니다.
제 경험상 우리는 패키지 단위가 아닌 마지막 순간에만 번들링함으로써 용량이 줄어드는 효과를 과대평가하고 있습니다. 이건 개별 사례별로 고려해야 할 사항이고 위에서 설명한 많은 이점들과 비교해봐야 합니다. (즉, 패키지별로 번들링 하지 않고 마지막에 번들링을 하면 디스크 공간이나 성능에서 절감 효과가 클 거라고 생각하지만 사실 그렇지 않다.)
납득됐어! 이제 뭘 해야 해?
모듈 번들러 사용법을 배우세요. (가능하면 Rollup이나 JSPM을 추천합니다. Webpack이나 Browserfy도 괜찮아요.) 그리고 npm의 prepublish 훅을 사용해 코드를 번들링 하세요. 친구들에게도 똑같이 하라고 알려주세요.
그리고 당신의 코드가 망가졌을 때 Azer 같은 사람을 비난하는 것을 멈추세요.
이 글을 찾은 것이 정말 기뻤습니다. 번들링 포스팅 글의 속편으로 복습에 탁월한 글이었습니다. 의존하고 있는 패키지 전부를 번들링해야하는 점에 대해 너무 명쾌하게 큰 사건과 연관지어서 설명해주었습니다. 물론 개별적으로 적용해보아야 하기 때문에 특정 케이스에서는 디버깅의 어려움이나 빌드 시간에 따른 단점이 생길 수 있을 거라고 생각이 됩니다. 제가 직접 Rollup을 사용해보면 좋을 듯 한데.. 다음에 비교할 수 있는 케이스를 들고올 수 있었음 합니다. 번들링에 대해 이렇게까지 생각해본 적이 없었는데 시간가는 줄 모르게 찾아보았고 더욱 관심이 가게 되었습니다. 다음 글로 Tree-shaking관점에서의 ES Modules와 CommonJS 차이점에 대해서도 설명할 때 이전 글이 연상될 수 있도록 하고 싶습니다. 번역이 이상하더라도 이해해주시길 바라며 정확한 의견을 보고 싶으시면 원본으로 보시는 걸 추천드립니다..
https://medium.com/@Rich_Harris/how-to-not-break-the-internet-with-this-one-weird-trick-e3e2d57fee28
https://en.wikipedia.org/wiki/Npm_left-pad_incident
https://github.com/left-pad/left-pad/issues/4
https://www.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/
'Development Environment' 카테고리의 다른 글
Why Vite? JS 모듈화의 역사, CJS, ESM, Webpack (2) | 2024.08.10 |
---|---|
Package Manager 패키지 매니저, pnpm (0) | 2023.08.09 |