How do modules help?
자바스크립트 프로그래밍은 결국 변수를 관리하는 것이다. 변수에 값을 할당하고, 변경하고, 여러 변수들의 값을 조합하여 다른 변수에 할당하는 작업이다. 그렇기 때문에 이러한 변수들을 관리하는 것이 좋은 코드와 유지 보수하기 쉬운 코드에 큰 영향을 미친다.
변수들을 관리할 때 한 번에 모든 변수를 생각하는 것보다 몇 개의 변수만 고려하는 것이 편하다. 자바스크립트의 scope
는 이를 가능하게 해준다. 함수는 다른 함수에 정의된 변수들에는 접근할 수 없다. 그러나 이러한 방법은 변수를 공유하고 싶을 때 문제가 생긴다. 이를 해결하기 위한 흔한 방법은 상위 스코프(글로벌 스코프)에 변수를 정의하는 것이다. 이러한 방법은 몇 가지 문제가 있다. 첫째, script
태그의 순서가 중요해진다. 순서를 바꾸거나, 특정 스크립트 태그를 지웠을 때 예상치 못한 에러가 발생할 수 있다. 둘째, 글로벌 스코프에 정의된 변수는 모든 함수가 접근할 수 있기 때문에 악성 코드에 취약하다. 악성 코드가 아니더라도 의도치 않게 변수의 값을 변경할 수 있다.
모듈은 이러한 문제들을 해결하고 변수들을 좀 더 쉽게 관리할 수 있도록 해준다. 모듈은 관련 있는 변수와 함수들의 묶음이라고 볼 수 있다. 이러한 module scope
내 함수들은 변수들을 공유할 수 있다. 또한, 함수 스코프와 다르게 모듈 스코프는 다른 모듈과 변수를 공유할 수 있는 방법이 존재한다. 모듈은 export
를 통해 특정 변수/클래스/함수를 다른 모듈에서 사용 가능하도록 만들 수 있다. 또한 import
를 통해 export
된 변수/클래스/함수에 의존한다는 것을 명시할 수 있다. 이러한 의존 관계는 명시적이기 때문에, 특정 모듈을 삭제했을 때 어떤 모듈이 작동하지 않을 것인지 알 수 있다.
현재 두 가지 모듈 시스템이 사용되고 있다. CommonJS(CJS)
는 Node.js 가 전통적으로 사용하던 모듈 시스템이다. ESM(EcmaScript modules)
는 자바스크립트에 새롭게 추가된 모듈 시스템이다. 브라우저는 ESM
을 지원하며, Node.js는 v13 이후 버전부터 지원한다고 한다. (아마도)
How ES modules work
모듈을 이용해 변수들을 관리하기 위해서는 모듈 인스턴스의 의존 그래프가 필요하다. 인스턴스는 코드(code)와 상태(state)를 결합한 것이다. 코드는 인스트럭션(instruction)의 집합이다. 즉, 무엇을 해야 할 지를 알려준다. 그러나 이러한 인스트럭션을 수행하기 위해서는 대상이 있어야 하는데, 그 대상이 바로 상태다. 상태는 특정 시간에 변수에 들어있는 실제 값을 의미한다. module loading
은 진입점 파일로부터 시작해 모듈 인스턴스의 전체 그래프를 만드는 과정을 말한다. 이는 세 단계로 진행된다.
- Construction - 필요한 파일을 찾아 다운로드하고, 모든 파일을
module record
로 만든다. 브라우저는 파일을 직접 사용할 수 없기 때문에 파일을 파싱해서module record
라는 자료구조로 바꾸는 과정이다. - Instantiation - module record를 인스턴스화한다. export된 값들을 메모리에 올리기 위해 메모리 안의 구역(box)을 찾는다. 그리고 exports 와 imports 가 메모리 안의 같은 구역을 가리키도록 한다. (이 때 변수들은 비어있다.)
- Evaluation - 코드를 실행하여 메모리 구역 안의 변수들을 실제 값으로 채운다.
각 단계를 조금 더 자세히 살펴보자.
Construction
- 모듈을 가지고 있는 파일의 위치를 찾는다. (module resolution)
- 파일을 fetch 한다. (URL을 통해 다운로드 받거나 파일시스템으로 부터 가져온다.)
- 파일을
module record
로 파싱한다.
위와 같은 과정을 수행하기 위해서는 한 파일을 fetch 한 후, 그 파일을 파싱하고, 그 파일이 의존하고 있는 모듈들을 알아내고, 그 모듈들이 있는 파일을 다시 fetch 하는 과정을 반복해야 한다.
모듈 그래프를 만드는 과정에서 메인 스레드를 블락하는 것은 모듈을 사용하는 앱을 느리게 만든다. 이것은 ES module
시스템이 construction 단계와 instantiation 단계를 나눈 이유 중 하나이다. construction 단계를 따로 분리함으로써 브라우저는 동기적인(메인 스레드를 블락하는) instantiation 단계 이전에 파일을 fetch 하고 모듈 그래프를 만드는 작업을 비동기적으로 수행할 수 있게 한다. 이렇게 단계를 분리하는 것은 ES module
과 CommonJS module
간의 중요한 차이점이다. 파일시스템으로부터 파일을 로드하는 것이 인터넷으로부터 다운로드하는 것보다 훨씬 빠르기 때문에 CommonJS
모듈 시스템을 사용하는 Node.js 는 파일을 로딩할 때 메인 스레드를 블락할 수 있다. 이는 모듈 인스턴스를 반환하기 전에 loading, instantiation, evaluation 이 모두 이루어진다는 의미이다.
💡 CommonJS
의 require()
모듈 파일은 해당 모듈에 의존하는 모듈이 여러개라도 한 번만 fetch된다. 이를 위해 module map
을 이용해 모듈 파일을 추적한다. 로더가 URL을 fetch할 때 모듈맵에 URL 을 적고, 현재 fetching 중임을 표시한다. 그리고 요청을 보낸 후 다음 파일을 fetch 하기 시작한다. 다른 모듈이 동일한 파일을 필요로 할 때, 로더는 모듈맵의 모든 URL을 살펴보고 fetching 중이면 다음 URL로 넘어간다. 모듈맵은 어떤 파일이 fetching 중에 있는 지 기록할 뿐만 아니라 모듈을 위한 캐시 또한 저장한다.
파일을 fetch 했으면 이제 파일을 module record
로 파싱해야 한다. 이는 브라우저가 파일을 이해할 수 있도록 해준다. module record
가 생성되면 이를 모듈맵에 넣는다. 이제 해당 모듈을 요청하면 모듈맵으로부터 제공할 수 있다.
Instantiation
위에서 언급했듯이 인스턴스는 코드와 상태를 합친 것이다. 상태는 메모리에 있으므로, instantiation 단계는 메모리와 연결하는 단계라고 할 수 있다. 먼저 JS engine은 module record
의 변수들을 관리하는 module environment record
를 생성한다. 그리고 모든 export
를 위한 메모리 구역을 찾는다. 모듈 환경 레코드는 메모리의 어느 구역이 어떤 export
와 관련되어 있는지 추적한다. 이 메모리 구역들은 아직 값이 할당되지 않는다.(값은 evaluation 단계에서 할당된다.) 여기서 중요한 것은 모든 export
된 함수 선언들은 이 단계에서 초기화된다는 것이다. 모듈 그래프를 인스턴스화하기 위해서 엔진은 post-order 깊이 우선 탐색을 진행하는데, 이는 그래프의 가장 아래, 즉 어느 것에도 의존하지 않는 모듈부터 시작해 export
를 셋업한다는 것을 의미한다. 모듈의 모든 export
를 메모리와 연결하면 그 다음 모듈의 import
를 메모리에 연결한다. export
와 import
는 메모리의 같은 위치를 가리킨다는 것을 기억하자. export
를 먼저 메모리에 올림으로써 import
가 매칭되는 export
와 연결될 수 있도록 해준다.
💡 CommonJS
와의 차이점
CommonJS
는 해당 객체가 export
될 때 전체가 복사된다. 즉, export
된 모든 값들은 복사본이다. 따라서 exporting 모듈에서 이후에 값을 바꾼다면 importing 모듈은 그 변화를 알 수 없다.
Evaluation
마지막 단계는 메모리 구역의 값을 채워넣는 단계이다. JS 엔진은 최상위 코드(top-level code)를 실행함으로써 이를 수행한다. 최상위 코드란 함수 바깥에 있는 코드를 말한다. 이 때 함수를 실행하면서 side effect가 발생할 수 있기 때문에, 각 모듈을 evaluate 하는 단계는 한 번만 실행되어야 한다. instantiation 단계에서 실행되는 linking은 여러 번 실행되어도 같은 결과를 반환하지만, evaluation 단계에서의 함수 실행은 그 횟수에 따라 결과가 달라질 수 있기 때문이다. 이는 모듈맵이 존재하는 이유 중 하나인데, 모듈맵은 모듈 파일의 URL로 모듈을 캐싱함으로써 각 모듈 당 하나의 module record
만 존재하도록 보장한다.
References
'Javascript Concepts' 카테고리의 다른 글
RequestAnimationFrame (0) | 2021.10.13 |
---|---|
Message Queue and Event Loop (0) | 2021.10.05 |
IIFE (0) | 2021.10.01 |
Scope (0) | 2021.09.28 |