반응형
Module CacheNode 에서 모듈 시스템은 CommonJS(CJS) & ECMAScript modules(ECM) 두가지가 있다.
Good Module System
- 파일의 분할: 구조적으로 코드를 구성하고 관리
- 코드의 재 사용성: 중복성 제거
- 은닉(캡슐)화: Public API 제공으로 접근성 제한
- 종속성 관리: 모듈들의 복잡한 종속성은 NPM으로 관리
CJS: Node.js의 File System에 직접 접근하는 특성을 이용. 오직 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 뿐이다.
CJS
- require : Local FS로부터 module import
- exports & module.exports를 사용해 모듈을 내보냄
require module importer operation logic
/* eslint no-eval: off */
/* eslint no-global-assign: off */
/**
In this particular implementation we are using the original
`resolve()` from Node.js `require`, therefore we have to do the following:
1. store a reference to the original require function (`originalRequre`)
2. redefine `require` by overriding the global `require` function
3. the new `require.resolve()` will essentially need to call the original require `resolve()`
*/
const originalRequire = require
const fs = originalRequire('fs')
function loadModule (filename, module, require) {
const wrappedSrc =
`(function (module, exports, require) {
${fs.readFileSync(filename, 'utf8')}
})(module, module.exports, require)`
eval(wrappedSrc)
}
require = function require (moduleName) {
console.log(`Require invoked for module: ${moduleName}`)
const id = require.resolve(moduleName) // ①
if (require.cache[id]) { // ②
return require.cache[id].exports
}
// module metadata
const module = { // ③
exports: {},
id
}
// Update the cache
require.cache[id] = module // ④
// load the module
loadModule(id, module, require) // ⑤
// return exported variables
return module.exports // ⑥
}
require.cache = {}
require.resolve = (moduleName) => {
// reuse the original resolving algorithm for simplicity
return originalRequire.resolve(moduleName)
}
// Load the entry point using our homemade 'require'
require(process.argv[2])
- id라 불리는 모듈의 전체 경로 확인
- loaded module의 경우 cache 사용
- 아니라면 빈 객체 리터럴을 통해 초기화 진행 ( module 객체는 public API export에 사용)
- module caching
- module 파일을 불러와 읽고 참조 등을 module에 전달
- 호출자에게 내용 반환
module.exports에 할당되지 않은 이상 모든 것은 private하다.
module.exports VS exports
exports는 module.exports의 초기값에 대한 참조일 뿐이다.
exports.hello = () => {
console.log('hello');
} // Possilbe
exports = () => {
console.log('hello');
} // Impossible
Require 함수는 동기 함수
Module을 비동기적으로 초기화하는 경우에 미처 초기화하지 못한 채 exports 될 수 있다.
Resolving Algorithm
모듈 경로가 코드를 고유하게 식별하게 해주는 알고리즘
File Module
- 모듈 경로가 / 로 시작 : 절대경로
- 모듈 경로가 ./ 로 시작 : 상대 경로
Core Module
- File Module이 아닐 경우 node.js 코어 모듈 내에서 탐색
Package Module
- Core Module도 아닐 경우 호출한 파일 경로부터 디렉터리를 같은 LV부터 타고 올라가 node_modules 폴더를 탐색
Module Cache
성능적 측면에서 중요하나, 순환 종속성 문제 발생 가능
- (11) 에서는 Cache된 b 모듈을 가져온다.
- (2)와 (3)에서 module b는 module a의 불완전한 모습을 참조하기에 문제가 발생하는 것.
이런 순환 종속성 문제는 ESM에서 해결됐다.
모듈 적재 방식이 생성 -> 인스턴스화 -> 평가 세 단계로 이루어진 후 코드가 실행되기 때문에 종속성 그래프가 깔끔하게 그려진다.
생성 단계에서 DFS로 import문을 찾기 때문에 이미 방문한 import 문에 대해선 재참조하지 않아 순환 종속성 문제를 해결!!
Module define pattern
Named exports
// module file
exports.info = (message) => {
console.log(`info: ${message}`)
}
exports.verbose = (message) => {
console.log(`verbose: ${message}`)
}
//-----------------------------------------------
// main file
const logger = require('./logger')
logger.info('This is an informational message')
logger.verbose('This is a verbose message')
Function exports ( Substack pattern )
// module file
module.exports = (message) => {
console.log(`info: ${message}`)
}
// exported function is used the other pulic API's namespace
module.exports.verbose = (message) => {
console.log(`verbose: ${message}`)
}
//-----------------------------------------------
// main file
const logger = require('./logger')
logger('This is an informational message')
logger.verbose('This is a verbose message')
명확한 진입점 & 단일기능 / 최소한의 노출로 SRP 원칙 준수 가능
SRP : Single Responsibility Principle
Class exports
// module file
class Logger {
constructor (name) {
this.name = name
}
log (message) {
console.log(`[${this.name}] ${message}`)
}
info (message) {
this.log(`info: ${message}`)
}
verbose (message) {
this.log(`verbose: ${message}`)
}
}
module.exports = Logger
//-----------------------------------------------
// main file
const Logger = require('./logger')
const dbLogger = new Logger('DB')
dbLogger.info('This is an informational message')
const accessLogger = new Logger('ACCESS')
accessLogger.verbose('This is a verbose message')
모듈 단일 진입점을 제공하고 훨씬 더 많은 모듈의 내부를 노출한다.
exported function을 다른 api의 namespace로 쓰는 방식을 사용할 때 더 개선할 수 있는 부분
Instance exports
// module file
class Logger {
constructor (name) {
this.count = 0
this.name = name
}
log (message) {
this.count++
console.log('[' + this.name + '] ' + message)
}
}
module.exports = new Logger('DEFAULT')
//-----------------------------------------------
// main file
const logger = require('./logger')
logger.log('This is an informational message')
// Not Recommended.
const customLogger = new logger.constructor('CUSTOM')
customLogger.log('This is an informational message')
서로 다른 모듈간 공유할 수 있는 상태 인스턴스 정의가 가능하다. ( 모듈이 캐시 되기 때문에 )
constructor 속성을 통해 새로운 객체 생성이 가능하다는 점에서 싱글톤 패턴은 아니다.
반응형
'Backend > Node.js' 카테고리의 다른 글
비동기 흐름 제어 by Callback (0) | 2025.01.06 |
---|---|
Observer 패턴 (0) | 2025.01.06 |
Callback 패턴 (0) | 2025.01.06 |
Module System ( ESM ) (0) | 2025.01.06 |
Node.js Interview (0) | 2024.12.30 |