JavaScript 번들러의 이해 — (1) JavaScript 모듈

권세규
네이버 플레이스 개발 블로그
5 min readJun 17, 2022

--

“JavaScript 생태계가 너무 복잡해요”
“Webpack 오류만 보면 경기를 일으킵니다”
“.d.ts가 도대체 뭐죠?”

JavaScript 생태계가 팽창하면서 웹 개발은 나날이 편해지고 있지만, 무작정 보고 따라하기 식 강의의 한계로 초심자들의 고민도 커지는 듯합니다.

이 시리즈에서는 JavaScript 생태계에 대한 이해를 하고, 모듈과 번들러를 이해하는 시간을 가져보도록 하겠습니다. 또한 2022년 기준의 생태계는 어떻게 진화하고 있는지도 알아보겠습니다.

옛날 옛적 호랑이 prototype 만지던 시절의 JavaScript에는 모듈이라는 개념 자체가 없었습니다. 웹 1.0까지만 하더라도 JavaScript의 역할은 아주 보잘것없었고, 웹 2.0으로 들어와서야 AJAX의 유행과 함께 점차 그 입지를 넓혀갔지요.

규모가 큰 프로그램을 개발할 때 모듈의 개념은 빠질 수가 없었고, 웹 어플리케이션 역시 예외는 아니었습니다. 관심사의 분리는 늘 필요하고, 이를 실현하기 제일 쉬운 방법은 파일을 쪼개는 것이었습니다.

그러나 이 방식은 다음과 같은 문제들이 있었습니다.

  • HTML 파일에서 파일들의 의존성을 수동으로 잡아줘야 합니다.
  • HTTP 요청이 증가합니다. 잦은 HTTP 요청은 오버헤드가 커서 서버에 부담을 주며, 스크립트 로드 시간이 증가하면 사용자 경험에도 악영향을 미칩니다.
  • 라이브러리 간 함수나 변수 같은 이름 충돌(Name Collision)이 발생할 수 있습니다.

네임스페이스 충돌은 즉시실행함수(IIFE, Immediately Invoked Function Expression)를 활용하여, 전역객체에 묶어서 내보내는 방식으로 해결했습니다.

하지만 나머지 문제는 여전히 해결하지 못했습니다.

이후 개발자들은 모듈을 정의하는 방법을 고안했고, 이 제안들을 수용하여 스크립트를 한 파일로 만드는 번들링(Bundling)이라는 개념이 탄생하게 됩니다.

번들링은 의존성을 자동으로 분석하여 번거로움을 덜어주고, 파일을 하나로 합쳐 HTTP 요청도 줄입니다. 게다가 번들링 과정에서 개발에 도움되는 다양한 기능들도 추가할 수 있어, JavaScript 생태계가 더욱 강력해지는 결과를 자아냈습니다.

번들러를 이해하려면, 그것들을 구성하는 JavaScript 모듈의 역사를 개략적으로 알아야 합니다. 모듈의 형식을 정의하고 합의하는 과정은 생각보다 순탄치 않았으며, 통일되기 전까지 난립하던 형식들이 번들러의 설계에 영향을 미쳤기 때문입니다.

CommonJS (=CJS)

초창기 개발자들은 각자가 다양한 방식을 제안했지만 결국 흐지부지되고 말았습니다. 그러던 중 2009년, Node.js의 태동과 함께 CommonJS라는 규격이 탄생합니다.

CommonJS는 require() 함수와 module.exports 를 사용하는 규격으로, 대표적인 구현체로 Node.js가 있습니다. 원래는 ServerJS라는 이름으로 시작되었으며, ECMA 2015가 등장하기 전까지는 사실상 표준이었습니다. Node.js 개발진의 입장에 따르면, 앞으로도 지원을 할 예정이라 합니다.

CommonJS는 ECMA 표준이 아니므로, 브라우저에서 제대로 처리할 수 없습니다. 브라우저 개발자 콘솔에서 exports 객체를 찾을 수 없다는 에러가 뜬다면, CommonJS 모듈이 번들링되지 않은 상태로 나갔다는 뜻입니다.

Asynchronous Module Definition (=AMD)

AMD는 CommonJS 논의 당시, 비동기 처리에 대한 합의점을 찾지 못한 개발자들이 따로 나와서 만든 규격입니다. 이들은 콜백을 활용한 기법을 제안했으며, CommonJS로 제작된 모듈도 쉽게 AMD로 변환 가능하다는 것을 장점으로 내세웠습니다. 대표적인 구현체로 RequireJS가 있으며, JQuery 초기버전에서도 그 흔적을 찾아볼 수 있습니다.

이 규격은 결국 사장되었지만, 후술할 UMD에 흔적을 남기게 됩니다.

Universal Module Definition (=UMD)

UMD는 CommonJS와 AMD 시스템을 모두 지원하기 위해 나온 규격이자 트릭입니다. 얼핏 보면 코드가 복잡해 보이지만, 자세히 보면 그리 이해하기 어렵지 않습니다. 윗부분은 런타임 환경을 인식한 뒤 규격에 맞게 분기하는 내용이며, 아랫부분은 AMD와 동일하게 이루어져 있습니다.

ECMA Script Module (=ESM)

이후 오랜 시간이 지나서 ECMA 2015 (=ES6) 표준에 드디어 모듈이 등장했습니다. TypeScript를 사용해봤다면 이미 익숙할 import 와 export 구문을 사용하는 규격이지요. 모던 브라우저라면 <script type=”module”> 을 명시해주기만 하면 사용할 수 있습니다.

간결함이 특징인 이 규격은 CommonJS와 다르게, export/import를 통해 객체(Object)만을 내보낼 수 있으며, 호출할 수 없다는 특징이 있습니다. 또한 default export는 내보내는 값에 제약이 없지만, 파일 당 하나만 가능하다는 조건이 있습니다.

CommonJS 진영인 Node.js 역시 13.2버전부터 실험적으로 ESM을 지원하기 시작했으며 아직까지는 개발경험이 매끄럽지 못하지만, 차차 나아지고 있는 추세입니다.

다음 글에서는 모던 웹 개발에 필수적인 TypeScript에서 모듈을 어떻게 처리하는지 알아보도록 하겠습니다.

다음 편 읽기

(2) TypeScript 모듈
(3) 번들러 개론
(4) Webpack 및 다른 번들러들

--

--