Node 에서 모듈 시스템은 CommonJS(CJS) & ECMAScript modules(ECM) 두가지가 있다.
Good Module System
- 파일의 분할: 구조적으로 코드를 구성하고 관리
- 코드의 재 사용성: 중복성 제거
- 은닉(캡슐)화: Public API 제공으로 접근성 제한
- 종속성 관리: 모듈들의 복잡한 종속성은 NPM으로 관리
CJS : Node.js의 FS에 직접 접근하는 특성을 이용 오직 Local JS File에 의존
ECM : Browser와 Server의 차이를 연결하기 위한 노력으로 탄생
Module Pattern
Revealing Module Pattern (RMP)
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "private" }] */
const myModule = (() => {
const privateFoo = () => {}
const privateBar = []
const exported = {
publicFoo: () => {},
publicBar: () => {}
}
return exported
})() // once the parenthesis here are parsed, the function will be invoked
console.log(myModule)
console.log(myModule.privateFoo, myModule.privateBar)
private과 public한 property를 구분해 public api 구성 요소를 설정할 수 있음
직접 접근 가능한 것은 exported object 뿐!
ESM
- 순환 종속성에 대한 지원과 비동기적 모듈 로딩을 지원
- Static 한 모듈 시스템으로 import는 반드시 모든 파일의 최상단에 있어야 함 ( 제어 구문 내부에서 사용 불가 )
사용을 원할 시 package.json에 "type" : "module" 추가 필요
CJS와 큰 차이점
- CJS: exports / module.exports 사용
- ESM: export 키워드만 사용
Named exports and imports
// module file
// exports a function
export function log (message) {
console.log(message)
}
// exports a constant
export const DEFAULT_LEVEL = 'info'
// exports an object
export const LEVELS = {
error: 0,
debug: 1,
warn: 2,
data: 3,
info: 4,
verbose: 5
}
// exports a class
export class Logger {
constructor (name) {
this.name = name
}
log (message) {
console.log(`[${this.name}] ${message}`)
}
}
//-----------------------------------------------------
// main file
// import all the members of the module
import * as loggerModule from './logger.js'
console.log(loggerModule)
// import a single member of the module
//-----------------------------------------------------
import { log } from './logger.js'
log('Hello World')
//-----------------------------------------------------
// import multiple members of the module
import { log, Logger } from './logger.js'
log('Hello World')
const logger = new Logger('DEFAULT')
logger.log('Hello world')
//-----------------------------------------------------
// name clash
import { log } from './logger.js'
const log = console.log // <- this would generate a "SyntaxError: Identifier 'log' has already been declared" error
log('Hello world')
//-----------------------------------------------------
// avoid name clash
import { log as log2 } from './logger.js'
const log = console.log
log('message from log')
log2('message from log2')
Default exports and imports
- export : named 이기 때문에 명시적
- default export
- default로 모듈이 나가기 때문에 호출자에 따라 이름이 다름 ( 비명시 )
- 모듈의 가장 핵심적 기능 한가지와 연결하는 편리한 방법
- 특정 상황에서 코드제거 ( tree shaking )를 어렵게 만듬 ( 모든 모듈의 내부를 참조하는 객체를 export default로 했다면 )
즉, 하나의 기능을 명시적으로 내보내고 싶을 때 export default 를 사용.
// module file
export default function log (message) {
console.log(message)
}
export function info (message) {
log(`info: ${message}`)
}
// -------------------------------------------------
// main file
import mylog, { info } from './logger.js'
mylog('Hello')
info('World')
비동기 import
import() 를 사용해 구현한다.
parameter로 모듈 식별자를 받고, 모듈 객체를 프로미스 객체로 반환한다.
const SUPPORTED_LANGUAGES = ['el', 'en', 'es', 'it', 'pl']
const selectedLanguage = process.argv[2]
if (!SUPPORTED_LANGUAGES.includes(selectedLanguage)) {
console.error('The specified language is not supported')
process.exit(1)
}
const translationModule = `./strings-${selectedLanguage}.js` // ①
import(translationModule) // ②
.then((strings) => { // ③
console.log(strings.HELLO)
})
ESM의 모듈 적재
interpreter의 최종목표 : 필요한 모든 모듈에 대해 종속성 그래프를 그리는 것
종속성 그래프를 통해 모든 모듈의 순서를 결정하게 해준다.
- 생성 ( parsing ) : 진입점 파일로부터 DFS로 필요한 모든 import 구문을 찾는다
- 인스턴스화 : exported object에 대해 명명된 참조를 메모리에 유지 후 이들간 종속성 관계를 추적한다.
- 평가 : js 코드 실행을 통해 인스턴스화된 객체를 실행시켜 값을 얻게 한 뒤 진입점 파일에서 코드를 실행한다.
1, 2 과정중에선 그 어떤 js 코드도 실행되지 않는다.
CJS와의 차이점 : CJS는 동적 성실로 종속성 그래프가 탐색되기 전에 모든 파일을 실행시킨다.
읽기 전용 라이브 바인딩
스코프 내에 개체가 임포트 됐을 때 코드의 직접 제어 영역 밖의 바인딩 값 ( 모듈의 코드 ) 은 원래 존재하던 모듈 ( live binding )에서 바뀌지 않는 한 원래의 값에 대한 바인딩이 변경 불가하다는 것. ( read-only binding )
CJS에서의 차이점 : CJS는 require시 exports 객체 전체가 얕은 복사가 돼 사용자가 이 값을 호출자의 실행 흐름 상에선 수정할 수 있고 이 사실을 모듈은 알 수 없다.
// module file
export let count = 0
export function increment () {
count++
}
// -------------------------------
// main file
import { count, increment } from './counter.js'
console.log(count) // prints 0
increment()
console.log(count) // prints 1
count++ // TypeError: Assignment to constant variable!
ESM에서의 참조 유실
파일 경로
ESM 에서는 import.meta 를 통해 현재 파일에 대한 참조를 얻을 수 있으며 특히 import.meta.url 은 현재 모듈을 참조한다.
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log({ __filename, __dirname })
console.log('import.meta', import.meta)
this keyword
- CJS : this === exports
- ESM : this === undefined
상호 운용
json file read
// import data from './data.json' // <- this would fail
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const data = require('./data.json')
console.log(data)
require 를 사용하지 않고 import 만으로 json 파일을 읽을 수 있게 됐다.
import with keyword
'Backend > Node.js' 카테고리의 다른 글
비동기 흐름 제어 by Callback (1) | 2025.01.06 |
---|---|
Observer 패턴 (1) | 2025.01.06 |
Callback 패턴 (0) | 2025.01.06 |
Module System ( CJS ) (0) | 2025.01.06 |
Node.js Interview (1) | 2024.12.30 |