JavaScript와 모듈 시스템

이예슬·2023년 2월 5일
0
post-thumbnail

최근 프로젝트를 진행하면서 번들러로 Vite를 사용했다. 늘 CRA만 사용했기 때문에 모듈 시스템과 번들러에 대한 이해가 부족했었는데 이번 기회에 해당 부분들에 대해 좀 더 공부해볼 수 있었다. 이번 포스팅에서는 그 중 JavaScript의 모듈 시스템에 대해 공부한 내용을 작성해보려고 한다.


모듈 시스템의 탄생 배경

JavaScript는 모듈이 없는 채로 탄생했다. JavaScript는 원래 웹 페이지 내 보조 작업을 처리하기 위한 언어였기 때문이다. JavaScript가 탄생할 때만해도 지금처럼 복잡한 웹 애플리케이션을 만들것이라고 상상하지 못했으므로 다른 프로그래밍 언어와 달리 모듈 시스템이 없었다.

그렇다면 모듈 시스템이 없다는 것은 어떤 의미였을까?

먼저 모듈이란 기능에 따라 파일별로 분리한 코드 조각을 말한다. 이때 각 모듈은 자신만의 모듈 스코프를 가져야 한다.

이러한 모듈 시스템이 없다는 것은 JavaScript 파일을 분리하여 작성하여도 브라우저 내에서 JavaScript는 한 파일 안에 있는 것처럼 전역(window)를 공유한다는 의미이다.

<html>
	<script src='/src/foo.js'></script>
	<script src='/src/bar.js'></script>
<html>

위와 같은 코드를 작성했을 때 foo.js에서 선언한 변수와 같은 이름을 가진 변수가 bar.js에 선언되어 있다면 나중에 호출된 bar.js의 변수로 재정의가 되면서 foo.js의 파일이 제대로 동작하지 않게 된다.

즉 모듈 간의 스코프가 구분이 되지 않아 다른 파일을 오염시킬 수 있다는 것이고 이는 코드를 유지보수함에 있어서 어려움을 준다.

브라우저 이외의 환경에서도 JavaScript를 사용하기 위해서는 모듈 시스템의 문제가 먼저 해결되어야 했다. 이에 JavaScript 모듈화를 위해 등장한 것이 commonJS, AMD이다.

CommonJS

CommonJS는 common이라는 이름에서부터 알 수 있듯이 JavaScript를 브라우저 뿐만이 아닌 서버사이드 애플리케이션이나 데스크톱 애플리케이션 등 좀 더 범용적인 환경에서도 사용하기 위해 조직한 자발적인 조직이다. 이 조직은 JavaScript를 범용적으로 사용하기 위해 필요한 명세를 만들며 해당 조직에서 제안한 방법을 commonJS라고 부른다.

module.exports = foo;

const foo = require('/foo');

CommonJS의 문법은 매우 간단하다. require로 모듈이 이미 로드되어 있는지 확인하고 인터페이스를 반환한다. 모듈 로더가 모듈 코드를 함수로 감싸기 때문에 모듈은 자동으로 자신만의 스코프를 갖는다. 디펜던시에 접근할 때는 require를 쓰면 되고 코드를 재사용할 때는 export를 사용하면 된다.

CommonJS는 서버 사이드 JavaScript 환경을 전제로 하며 이는 모든 파일이 로컬 디스크에 있어 필요할 때 바로 불러올 수 있는 상황을 전제로 한다는 의미이다.

이러한 방식을 브라우저에서 사용할 경우 필요한 모듈을 모두 내려받을 때까지 아무런 동작도 할 수 없다. 즉 비동기 방식보다 느리고 트리 쉐이킹(tree shaking)이 어렵다는 단점이 존재했다. 그럼에도 불구하고 모듈 시스템은 필요한 부분이었으므로 JavaScript의 런타임인 Node.js는 commonJS 방식을 선택했고 대부분의 npm 패키지들도 CommonJS의 방식을 따르고 있다.

AMD(Asynchronous Module Definition)

위에서 말했듯 commonJS는 비동기를 지원하지 않는다. AMD는 비동기 상황에서도 JavaScript 모듈을 사용하기 위해 등장했다.

define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {  
exports.verb = function() {

// 넘겨받는 인수를 사용해도 되고
return beta.verb();

// 또는 require()를 이용해
// 얻어 온 모듈을 사용해도 된다.
return require("beta").verb();  
}

commonJS와 비교해 상대적으로 문법이 복잡하지만 AMD의 모듈 명세는 비동기 환경에서도 잘 동작하며 비동기적으로 모듈을 호출하는 특성 때문에 퍼포먼스 면에서도 CommonJS 보다 나은 성능을 보였다.

ES Module

CommonJS와 AMD은 서로 다른 방식으로 모듈 시스템을 구현하고자 했고 이처럼 통일되지 않은 방식은 호환성 문제로 이어졌다. 이를 해결하고자 UMD(Universal Module Definition) 패턴이 등장했지만 이 또한 결국 모듈 시스템의 부재라는 근본적인 문제를 해결하지는 못했다.

이와 같은 문제를 해결하기 위해 2015년에 표준 JavaScript에서도 ES modules라는 독자적인 모듈 시스템을 추가했다.

import foo from 'bar';

export default qux;

ES module은 동기/비동기 로드를 모두 지원하며 문법또한 간단하다.

ES module의 동작은 아래와 같이 세 단계로 이루어진다.

  1. 구성 : 모든 파일을 찾아 다운로드하고 모듈 레코드로 구문분석한다.
  2. 인스턴스화 : export된 값을 모두 배치하기 위해 메모리에 있는 공간들을 찾는다. 그 다음 export와 import들이 이런 메모리 공간들을 가리키도록 한다. 이를 연결(linking)이라고 한다.
  3. 평가 : 코드를 실행하여 상자의 값을 변수의 실제 값으로 체운다.

각 단계별로 이뤄지는 자세한 과정은 이 글에서 자세하게 설명하고 있으니 더 알고 싶다면 참고하자.


https://yozm.wishket.com/magazine/detail/1261/
https://d2.naver.com/helloworld/12864

https://roseline.oopy.io/dev/javascript-back-to-the-basic/module-system

profile
꾸준히 열심히!

0개의 댓글