모듈이 필요한 이유
1. 코드베이스를 나누어 여러 파일로 분할해서 코드를 좀 더 구조적으로 관리. 각각의 독립적인 기능의 조각들을 개발 및 테스트하는데 도움.
2. 다른 프로젝트에 코드를 재사용 가능
3. 은닉성을 제공
4. 종속성을 관리. 필요한 일련의 종속성들을 쉽게 임포트.
CommonJS 내부와 모듈 패턴
CommonJS는 Node.js의 첫 번째 내장 모듈 시스템입니다. CommonJS 명세의 두 가지 주요 개념을 요약하면 다음과 같습니다.
- require는 로컬 파일 시스템으로부터 모듈을 임포트하게 해줍니다.
- exports와 module.exports는 특별한 변수로서 현재 모듈에서 공개될 기능들을 내보내기 위해서 사용됩니다.
module.exports vs exports
많은 사람들이 module.exports와 exports의 차이를 혼란스러워 하는데 require 함수를 통해 이 차이점을 명확하게 이해할 수 있습니다.
변수 exports는 module.exports의 초기 값에 대한 참조일 뿐입니다. 이 값은 모듈이 로드되기 전에 만들어지는 간단한 객체 리터럴입니다.
즉, exports가 참조하는 객체에만 새로운 속성을 추가할 수 있습니다.
exports.hello = () => {
console.log("Hello");
}
exports 변수의 재할당은 module.exports의 내용을 변경하지 않기 때문에 아무런 효과가 없습니다. 그것은 exports 변수 자체만을 재할당합니다. 따라서 아래 코드는 잘못된 것입니다.
exports = () => {
console.log("Hello")
}
함수, 인스턴스 또는 문자열과 같은 객체 리터럴 이외의 것을 내보내려면 다음과 같이 module.exports를 다시 할당해야 합니다.
module.exports = () => {
console.log("Hello)
}
동기적인 require 함수
require() 함수가 동기적이고 모듈 내용을 반환하므로 콜백이 필요하지 않습니다. 그 결과 module.exports에 대한 할당도 역시 동기적이어야 합니다. 아래 예시는 올바르지 않습니다.
setTimeout(() => {
module.exports = function () {...}
}, 100)
동기적 특성을 지닌 require()는 모듈을 정의할 때 동기적으로 코드를 사용하도록 제한함으로써 우리가 모듈을 정의하는 방식에 영향을 미칩니다. 이것은 Node.js의 핵심 라이브러리가 비동기 방식에 대한 대안으로 동기식 API를 제공하는 가장 중요한 이유들 중 하나입니다.
함수 내보내기
가장 일반적인 모듈 정의 패턴 중 하나가 module.exports 변수 전체를 함수로 재할당하는 것입니다.
주요 장점은 모듈에 대한 명확한 진입점을 제공하는 단일 기능을 제공하여 그것에 대한 이해와 사용을 단순화 하는 것입니다. 또한 최소한의 노출이라는 원리에 잘 맞아 떨어집니다. -> 이 방법은 서브스택 패턴으로 알려져 있습니다.
// logger.js 파일
module.exports = (message) => {
console.log(`infoL: ${message}`)
}
아래 코드는 export 된 함수를 네임스페이스로 사용하여 앞에 정의한 모듈의 확장을 보여줍니다.
module.exports.verbose = (message) => {
console.log(`verbose: ${message}`)
}
아래 코드는 방금 정의한 모듈을 사용하는 방법을 보여줍니다.
// main.js 파일
const logger = require('./logger')
logger('This is an informational message')
logger.verbose('This is a verbose message')
함수를 내보내는 것은 단일 기능에 중점을 두도록 하는 완벽한 방법이며, 내부 형태에 대한 가시성을 줄이면서 이외 보조적인 사항들을 export 된 함수의 속성으로 노출하여 단일 진입점을 제공합니다.
Node.js의 모듈성은 한 가지만 책임지는 원칙을 지킬 것을 강력히 권장합니다.
모든 모듈은 단일 기능에 대한 책임을 져야 하며, 책임은 모듈에 의해 완전히 캡슐화되어야 합니다.
ES 모듈(ESM)
ECMAScript과 CommonJS 사이의 가장 큰 차이점은 ES 모듈은 static이라는 것입니다. 즉, import 가 모든 모듈의 가장 상위 레벨과 제어 흐름 구문의 바깥쪽에 기술됩니다. 또한 import할 모듈의 이름을 코드를 이용하여 실행 시에 동적으로 생성할 수 없으며, 상수 문자열만이 허용됩니다.
Node.js에서 ESM의 사용
Node.js는 모든 .js 파일이 CommonJS 문법을 기본으로 사용한다고 생각해서 .js 파일에 ESM 문법을 사용한다면 인터프리터는 에러를 낼 것입니다. CommonJS 모듈 대신 ES 모듈을 받아들일 수 있는 몇 가지 방법이 있습니다.
- 모듈 파일의 확장자를 .mjs로 합니다.
- 모듈과 가장 근접한 package.json의 "type" 필드에 "module"을 기재합니다.
exports와 imports 지정하기
ESM은 export 키워드를 통해 모듈의 기능을 export 하게 해줍니다.
(ESM은 CommonJS에서 여러 방법(exports와 module.exports)을 사용하는 것과는 다르게 export 한 단어만 사용합니다.
ES 모듈에서는 기본적으로 모든 것이 private이며 export된 개체들만 다른 모듈에서 접근 가능합니다.
export 키워드는 우리가 모듈 사용자에게 접근을 허용하는 개체 앞에 사용합니다.
예제를 보면
// logger.js
//`log`로서 함수를 export
export function log(message){
console.log(message)
}
//`DEFAULT_LEVEL`로서 상수를 export
export const DEFAULT_LEVEL = 'info'
// `LEVELS`로서 객체를 export
export const LEVELS = {
error: 0,
debug: 1,
warn: 2,
data: 3,
info: 4,
verbose: 5
}
//`Logger`로서 클래스 export
export class Logger{
constructor(name){
this.name = name;
}
log(message){
console.log(`[${this.name}] ${message}`)
}
}
import 시에는 import 되는 개체가 현재의 스코프로 import 되기 때문에 이름이 충돌하면 코드가 동작하지 않습니다.
import { log } from './logger.js'
const log = console.log
이런 경우는 개체의 이름을 as 키워드로 바꿔서 문제를 해결할 수 있습니다.
import { log as log2 } from './logger.js'
const log = console.log
log('message from log')
log2('message from log2')
export와 import 기본값 설정하기
CommonJS에서 가장 많이 사용되는 특성은 이름이 없는 하나의 개체를 module.exports에 할당하여 export 할 수 있다는 것입니다. ESM에서도 비슷한 동작을 할 수 있는데, default export 라고 불립니다.
// logger.js
export default class Logger{
constructor(name){
this.name = name;
}
log(message){
console.log(`[${this.name}] ${message}`)
}
}
이 경우 Logger라는 이름은 무시되며, export 되는 개체는 default 라는 이름 아래 등록됩니다. export 된 이름은 특별한 방법으로 다뤄집니다.
// main.js
import MyLogger from './logger.js'
const logger = new MyLogger('info')
logger.log('Hello World')
default export는 이름이 없는 것으로 간주되기 때문에 이름을 명시한 ESM의 import 와는 다릅니다. import 와 동시에 우리가 지정한 이름으로 할당됩니다.
혼합된 export
ES 모듈에서는 이름이 지정된 export와 default export를 혼합하여 사용 가능합니다. 예제를 살펴보면
// logger.js
export default function log(message){
console.log(message)
}
export function info(message){
log(`info: ${message}`)
}
위의 코드를 보면 log() 함수가 default export로서 내보내지고 info() 함수는 이름을 가진 export로 내보내집니다.
default export와 이름을 가진 export를 import하기 원한다면
import mylog, { info } from './logger.js'
비동기 임포트
import 구문은 정적이기에, 두 가지 주요 제약이 존재합니다.
- 모듈 식별자는 실행 중에 생성될 수 없습니다.
- 모듈의 import는 모든 파일의 최상위에 선언되며, 제어 구문 내에 포함될 수 없습니다.
CommonJS와 ESM 사이의 차이점 및 상호 이용
ES 모듈들은 암시적으로 strict mode에서 실행됩니다. 명시적으로 'use strict' 구문을 모든 파일의 시작에 추가할 필요가 없다는 의미와 같습니다. strict mode를 해제할 수 없지만 더 안전한 실행 모드이기 때문에 확실히 좋은 것이라고 할 수 있습니다.
ESM에서의 참조 유실
ESM에서는 ES 모듈이 strict mode에서 실행되기 때문에 require, export, module, exports, __filename 그리고 __dirname 을 포함하여 CommonJS 의 몇 가지 중요한 참조가 정의되지 않습니다. 만약 우리가 ES 모듈에서 이러한 것들을 사용한다면 ReferenceError가 발생합니다.
console.log(exports) // ReferenceError: exports is not defined
console.log(module) // ReferenceError: module is not defined
console.log(__filename) // ReferenceError: __filename is not defined
console.log(__dirname) // ReferenceError: __dirname is not defined
__filename 은 현재 모듈 파일의 절대 경로를 __dirname 은 파일이 있는 폴더의 절대 경로를 나타냅니다. 이러한 특별한 변수들은 우리가 현재 파일에 대한 상대 경로를 빌드할 때 매우 유용합니다.
ESM에서는 특별한 객체인 import, meta 를 사용하여 현재 파일에 대한 참조를 얻을 수 있습니다. 특히, import,meta,url 은 현재 모듈을 참조하며 file:///path/to/current_module.js 와 같은 형식입니다. 이 값은 절대 경로 형식에 대한 __filename과 __dirname를 재구성하는데 사용할 수 있습니다.
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
또한 require() 함수를 다음과 같이 재구성하는 것이 가능합니다.
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
또 다른 차이점은 this 키워드의 동작입니다.
ES 모듈의 전역 범위에서 this 는 undefined인 반면, CommonJS에서는 this가 exports와 같은 참조를 하고 있습니다.
// this.js - ESM
console.log(this) // undefined
// this.cjs - CommonJS
console.log(this === exports) // true
상호운용
ESM에서 표준 import 문법을 사용하여 CommonJS 모듈을 import하는 것 또한 가능합니다. 그러나 이는 deafult exports에 한정됩니다.
import packageMain from 'commonjs-package' // 작동
import { method } from 'commonjs-package' // 에러
안타깝게도 CommonJS 모듈에서 ES 모듈을 임포트하는 것은 불가능합니다.
또한 CommonJS에서 꽤 자주 사용되는 기능인 JSON 파일을 직접적으로 가져오기는 ESM에서 불가능합니다. 따라서, 다음 import 구문은 동작하지 않습니다.
import data from './data.json'
위의 코드는 TypeError(Unknown file extension: .json)를 발생합니다.
이러한 제한을 극복하기 위해서 module.createRequire 유틸을 사용할 수 있습니다.
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const data = require('./data.json')
console.log(data)
ESM에서도 JSON 모듈을 그대로 지원하기 위한 작업은 진행 중입니다. 따라서 우리는 가까운 미래에 createRequire()에 의존할 필요가 없을 수도 있습니다.
'Javascript > Node.js' 카테고리의 다른 글
Promise.all() (0) | 2021.11.02 |
---|---|
[mac/vscode] 프로젝트마다 node 버전 다르게 사용하는 방법 (0) | 2021.10.26 |
promise API (0) | 2021.09.28 |
콜백 (0) | 2021.09.27 |
커스텀 에러와 에러 확장 (0) | 2021.09.27 |