2022. 4. 23. 17:49ㆍ책/Node.js 디자인 패턴 바이블
6. ESM: ECMAScript 모듈
ECMASciprt 2015 명세의 일부분으로 JS의 공식 모듈 시스템을 구축하기 위해 도입됐다. CommonJS나 AMD의 기존 모듈 시스템內 장점은 그대로 두되, 간단한 문법과 비동기적 모듈 로드, 순환참조 지원 등 여러 이점을 제공한다.
CommonJS와 가장 큰 차이점은 ESM에선 모듈이 static이라는 것이다. static이란게 무슨 말일까? 쉽게말해 아래와 같이 선언하란 뜻인데, 구체적으론 아래와 같다.
import foo from "./mod.js";
- 어떤 코드 중간에서 import할 수 없으며, import 선언은 최상위 레벨에서 선언되어야 한다
- 어디에서(from) 어떤 모듈을 불러올 것인지(import) 명시적으로 작성해야 한다
- 즉, 코드의 실행 도중에 평가되어 불러오는게 아닌 parsing과정에서 정적으로 불러오게 된다는 뜻.
왜 이런 제약을 만들어둔 것일까? static import를 사용하면 CommonJS의 동적인 특성에서 파생되는 비효율을 개선할 수 있기 때문인데, tree shaking같은 코드 최적화에 활용될 수 있다(지금 당장은 이해하지 못해도 상관없음)
※ What do we mean by "static"?
When you use an import declaration, it must be at the top level of the module, outside of any control-flow statements, and it must use a string literal to say what module to import from, not just a string. That's so the relationship between modules can be determined by static analysis (by just parsing, not running, the code). This is valid:
1) ESM 사용하기
사실 Node.js에선 CommonJS 문법이 기본이라 ESM 문법을 사용하고 싶다면 아래 방법을 적용해야 한다
- 파일 확장자를 .mjs로 지정한다
- package.json의 "type" 필드를 "module"로 설정한다
위 방법 없이 그냥 import 문법을 사용하면 다음과 같은 워닝 메세지를 보게 될 것이다. Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
참고로 ESM에선 기본적으로 모든 것이 private이며, export된 개체들만 다른 모듈에서 접근 가능하다.
2) named exports, imports
내보내고자 하는 개체에 이름을 달아줘서 export하고, 그 이름대로 import하는 방식.
// 모듈1.js
export const PORT = 8080; // PORT 내보내기
export const URL = 'url이지롱'; // URL 내보내기
// main.js
// 모듈1.js에 있는 PORT, URL 가져오기
import { PORT, URL } from './모듈1.js'
모듈에 있는 모든 내용을 가져오고 싶다면 * 키워드를 사용하면 되고, 따로 이름을 달아줘서 import하고 싶다면 as 키워드를 사용하여 별도의 네임스페이스에 담아서 import할 수 있다.
// logger.js
// 함수 내보내기
export function log(msg) {
console.log(msg)
}
// 상수 내보내기
export const DEFAULT_LEVEL = 'info';
// 객체 내보내기
export const LEVELS = {
error: 0,
debug: 1,
warn: 2,
data: 3,
info: 4,
verbose: 5,
};
// 클래스 내보내기
export class Logger {
constructor(name) {
this.name = name;
}
log(msg) {
console.log(`메시지 : ${msg}`)
}
}
// main.js
// ESM에선 모듈 로드시 .js라고 파일 확장자를 명시해줘야 한다
import * as loggerModule from './logger.js';
// loggerModule이란 네임스페이스를 하나 만들어서
// 거기에 전부 다 담아서 로드시켜버리기
console.log(loggerModule);
[Module: null prototype] {
DEFAULT_LEVEL: 'info',
LEVELS: { error: 0, debug: 1, warn: 2, data: 3, info: 4, verbose: 5 },
Logger: [class Logger],
log: [Function: log]
}
3) default exports, imports
CommonJS에서 가장 많이봤을 패턴중 하나는 module.exports로 특별히 이름을 안달아주고 내보내는 패턴이다.
- 그러니까 module.exports = msg => console.log(`메세지 : ${msg}`); 이런 패턴
ESM에서도 비슷한 동작이 가능한데, 이를 default export라고 부른다. export되는 개체를 default란 이름 하에 등록하고 내보내는 방식인데, 특별한 이름이 없는 것으로 간주되기 때문에 import시 이름을 하나 명시해주면 그 이름으로 곧바로 할당된다. 말이 어려울지도 모르겠는데, 그냥 밥먹듯이 봤던 패턴일 것이다.
// logger.js
export default class Logger {
constructor(name) {
this.name = name;
}
log(msg) {
console.log(`[${this.name}] : ${msg}`);
}
}
// main.js
import Logger from './logger.js';
const logger = new Logger('삼성');
logger.log('삼성가고싶다')
// default란 이름으로 export한거기 때문에
// 아래 방식도 당연히 동작한다
import * as myLogger from './logger.js'
const logger2 = new Logger('LG');
import { default } from './logger.js';
// SyntaxError: Unexpected reserved word
// default란 이름에 달아서 export한게 맞긴한데
// default란 이름의 변수는 사용할 수 없다
4) mixed exports
대단한건 아니고 named export와 default export를 한 모듈 내에서 혼합해서 사용할 수 있다는 뜻.
모듈 내에서 둘 다 사용할 수 있다는데 그래서 named export와 default export의 차이는 뭘까?
- named export는 명확하다. 즉, 지정된 이름을 갖기 때문에 IDE에서 자동 임포트, 자동 완성, 리팩토링 툴을 지원할 수 있게 한다. 이게 무슨 소린가 싶을텐데 위 예시에서 import { log 까지만 쳐도 자동완성 기능이 나타나면서 구구절절 다 안쳐도 되는 상황을 만났을 것이다.
- default export는 가장 핵심적인 기능 하나를 골라서 내보낸다는 성격이 강하다.
- default export는 사용하지 않는 코드의 제거(tree shaking)가 어렵다. 예를 들어 10개의 함수를 가진 객체를 export default로 내보냈고, 이를 import해서 쓰고있지만 그중 3개의 기능만 쓰고있다쳐도 나머지 7개의 함수만 따로 쳐내기 힘들다.
5) 모듈 식별자(module identifier)
import 문에서 불러오고 싶은 모듈의 경로를 명시하는 값. 통상 상대경로를 이용해 모듈을 불러오지만 사실 더 다양한 방법으로 불러오는게 가능하긴 하다.
- 상대적 식별자(Relative) : ./logger.js 혹은 ../lib/logger.js 같은 상대적 경로 사용
- 절대 식별자(Absolute) : file:///opt/nodejs/config.js 같은 완전한 경로 사용. /나 //가 선행하면 동작하지 않는다. 이 방법은 CommonJS에선 지원하지 않으며 ESM만의 특징이다.
- 노출 식별자(Bare) : fastify나 http와 같이 node_modules 폴더에서 사용 가능하고 패키지 매니저를 통해 설치된 모듈 또는 Node.js 코어 모듈을 가리킴
- 심층 임포트 식별자(Deep import) : fastify/lib/logger.js 처럼 node_modules에 있는 패키지의 경로를 가리킴
6) 비동기 import
7) 모듈 적재
7. ESM과 CommonJS의 차이점, 상호 운용
'책 > Node.js 디자인 패턴 바이블' 카테고리의 다른 글
[2장] 모듈 시스템(3) (0) | 2022.04.20 |
---|---|
[2장] 모듈 시스템(2) (0) | 2022.04.20 |
[2장] 모듈 시스템 (0) | 2022.04.19 |
[1장] Node.js 플랫폼 (0) | 2022.04.16 |