CommonJS? ESM?

Chang-__-·2023년 3월 6일
0

Common Js 모듈 (cjs)

Common JS 는 Node.js 의 첫 번째 내장 모듈 시스템이다.
크게 두가지 주요개념은

1. require => 파일 시스템으로 부터 모듈을 임포트 한다.
2. exports, moudule.exports => 모듈에서 공개될 기능을 내보내기 위해서 사용한다.

이렇게 두 가지 이다.

가져오는 코드는

const hello = require('hello')

이렇게 작성 될 수 있고

export 되는 코드는

exports.hello = () => {
	console.log('hello');
} 

module.exports = () => {
	console.log('hello');
}

이렇게 두가지 방법으로 정의 할 수 있다.

중요한건 CommonJS 에서 모듈을 로드 하는 방식은 동기적으로 로드를 해온다.
만약에 모듈을 비동기적으로 로드 해오면, 미래 시점에 비동기적으로 초기화되기 때문에 초기화되지 않은 모듈을 정의하고 export 할 수도 있기 때문이다.

앞서 언급하였듯 CommonJS 에서 모듈을 가져올 땐 require 키워드를 통해 가져온다.
이 키워드는 어떻게 동작할까?

1. 모듈 이름을 받아 모듈의 전체경로를 알아내는 resolve 알고리즘이 실행 될 것이다.
2. 모듈이 이미 로드된 경우 캐시를 사용한다.
3. 모듈이 아직 로드 되지 않았다면 최초로드를 한다.
4. 최초로드 후 캐시를 한다.
5. module.exports의 내용이 호출자에게 반환된다.

이렇게 작동하는데 여기서 resolve 알고리즘과 캐시에 대해 조금만더 드릴다운 해보자.

resolve 알고리즘

  1. 파일 모듈: / 이나 ./ 키워드가 들어가면 로컬시스템에서 계신을 한다.
  2. 코어 모듈: / 이나 ./ 키워드가 들어가지 않으면 알고리즘은 코어 Node.js 모듈 검색을 시도 한다.
  3. 패키지 모듈: / 이나 ./ 키워드가 들어가지 않고 코어 모듈에도 없으면 패키지 모듈에서 검색을 시도 한다.

모듈 캐시

모듈은 처음 로드될 때만 캐시를 한다. 성능을 위해서도 중요하지만 다른 부가적인 역할도 있다.
1. 모듈 종속성 내에서 순환을 가질 수 있음.
2. 패키지 내에서 동일한 모듈이 필요할 때 동일한 인스턴스가 반환된다는 것을 보장 하기 위해.

ES 모듈 (mjs)

ESM 과 CommonJS 의 가장 큰 차이점은 ES 모듈은 static 이라는 것이다.
import 가 모든 모듈의 상위 레벨과 제어 흐름 구문의 바깥쪽에 기술된다.
모듈을 코드를 사용햐서 실행 시에 동적으로 생성할 수 없으며, 상수 문자열만이 허용된다.
EMS 에서는 모듈을 export 할 때는 export 한 단어만 사용한다. 반대로 모듈을 가져올 때는 import 를 활용하여 가져온다.

export class Logger {
	constructor(name) {
    	this.name = name;
    }
    
    log(message){
    	console.log(`${this.name} ${message}`);
    }
}

이렇게 Logger 클래스를 만들고 Logger를 가져와서 사용 할 때는

import { Logger } from 'logger.js'
const logger = new Logger('Hello')
logger.log('World!')

이렇게 사용 할 수 있다.

재미있는건 ESM 는 비동기 import 도 지원을 한다.

import(module).then(() => {...})

CommonJS 에선 정적으로 밖에 로드를 못해왔는데 ESM 에선 동적으로 로드를 해 올 수 있다.

사실 CommonJS와 ESM 에 가장 크게 차이나는 것은 모듈을 로드하는 방법이다.
ESM의 로드 방법은 다음과 같다.

1. 생성: 모든 import 구문을 찾고 재귀적으로 각 파일로부터 모든 모듈의 내용을 적재한다.
2. 인스턴스화: 익스포트된 모든 개체들에 대해 참조를 메모리에 유지한다. 
			import 와 export 문에 대한 참조가 생성되어 종속성 관계를 추적한다.
3. 평가: Node.js 는 코드를 실행하여 인스턴스화된 모든 개체가 실제 값을 얻게 한다.

CommonJS 랑 얼핏 보면 많이 달라 보이진 않는데, CommonJS는 종속성 그래프가 탐색되기 전에 모든 파일을 실행시키는 반면, ESM에선 종속성 그래프가 완전해지기 전까진 어떤 코드도 실행시키지 않는다.

순환종속성


이런 구조로 모듈을 사용한다고 가정을 했을 때 CommonJS 모듈과 ESM 에선 어떻게 사용될지 순환 종속성에 대해 드릴다운 해보겠다.

CommonJs 에서의 순환 종속성

모듈 a.js

exports.loaded = false;
const b = require('./b')
module.exports = {
	b,
    loaded: true
}

모듈 b.js

exports.loaded = false;
const a = require('./a')
module.exports = {
	a,
    loaded: true
}

main.js

const a = require('./a')
const b = require('./b')

console.log('a -> ', JSON.stringify(a, null , 2))
console.log('b -> ', JSON.stringify(b, null , 2))

이런 구조 일 때 다음과 같이 동작한다.

1. main.js 에서 a.js 를 불러온다.
2. a.js 는 loaded: false 로 초기화 된다. 
3. a.js 는 b.js 를 불러온다. 
4. b.js 는 loaded: false 로 초기화 된다.
5. b.js 는 a.js 를 불러온다.
6. a.js 는 이미 한번 초기화 되어서 캐싱이 됬기 때문에 값을 복사한다.
7. loaded: true로 바꿔준다.
8. b.js 의 값이 초기화 되어 캐싱이 되어서 b.js의 값을 복사 해온다.
9. loaded: true로 바꿔준다.
10. 컨텍스트가 main.js 로 반환되고 현재 모듈 a.js 의 상태를 복사하여 내부 범위에 갖는다.
11. main.js가 b.js를 불러온다.
12. 캐싱이 된 b.js 의 값을 return 해서 받아온다.

여기서 문제는 모듈 b.js가 모듈 a.js 의 완전하지 않은 상태를 바라보게 되고, b.js 가 main.js 에서 require 될 때 캐싱이 되어 완전하지 않은 값을 바라보게 된다는 문제점이 있다.
이 부분은 ESM에서 잘 풀었다.

ESM 에서의 순환 종속성

a.js

import * as bModule from './b.js';
export let loaded = false;
export const b = bModule;
loaded = true;

b.js

import * as aModule from './a.js';
export let loaded = false;
export const a = aModule;
loaded = true;

main.js

import * as a from './a.js';
import * as b from './b.js';
console.log('a -> ', a);
console.log('b -> ', b);

ESM은 위에서 언급 하였듯이 파싱 -> 인스턴스화 -> 평가 단계를 거치는데 각 페이즈에서 설명 해보겠다.

  1. 파싱
1. main.js 에서 a.js로 간다.
2. a.js 에서 b.js 로 간다.
3. b.js 에서 a.js 로 가는데, 이미 방문해서 다시 방문하지 않는다.
4. main.js 에서 b.js로 가는데 b.js 는 이미 방문해서 다시 방문하지 않는다.
  1. 인스턴스화

    인스턴스화를 할 때는 아래에서 위로 이동한다.
1. b.js 는 aModule라는 이름으로 a.js의 export와 연결한다.
2. a.js 는 bModule라는 이름으로 b.js의 export와 연결한다.
3. main.js 는 a 와 b 라는 이름으로 a.js, b.js 의 export 와 연결한다.
현재 상태는 인스턴스화 되지 않은 상태이다. 각각의 참조의 링크만 걸린상태.
  1. 평가

    파싱의 실행순서 또한 원래의 종속성 그래프에서 DFS 탐색으로 아래에서 위로 올라간다.
1. b.js 부터 시작되며, loaded: false 로 시작한다.
2. a property 가 평가 되며, a.js의 객체의 참조로 평가 된다.
3. loaded: true 로 바뀐다. 이 때 b.js의 export 상태가 완전히 평가되었다.
4. a.js 로 이동되고, loaded: false 상태로 시작한다.
5. b property 가 평가 되며, b.js의 객체의 잠조로 평가된다.
6. loaded: true 로 바뀐다. 이 때 a.js의 export 상태가 완전히 평가되었다.

이 때 export 된 모든 속성들은 완전히 평가된 상태이다. import 된 모든 모듈들은 참조로 추적이 되고 순환 종속성이 존재하는 상황에서도 모든 모듈이 다른 모듈의 최신 상태를 갖고 있는 것이 자명해졌다.

0개의 댓글