🗓️ 2024. 02. 18
⏱️ 8

Node.js에서 메인 모듈 식별하기

알아두면 쓸만해요

메인 모듈?

Node.js에서 메인 모듈은 프로세스의 진입점이 되는 파일을 뜻합니다.
예를 들어 node app.js로 프로세스가 실행되면 이 프로세스의 메인 모듈은 app.js 입니다.

파이썬을 경험해보신 분이라면 익숙한 개념일 텐데요, 파이썬 예제를 잠깐 살펴보겠습니다.

# greeting.py
def greeting():
    print("Hello, World!")


if __name__ == "__main__":
    print("Hi, I am greeting module")
# app.py
from greeting import greeting


greeting()
$ python greeting.py
Hi, I am greeting module

$ python app.py
Hello, World!

이처럼 파이썬에서는 if __name__ == "__main__"으로 런타임에서 메인 모듈을 식별할 수 있습니다.
Node.js에선 어떻게 런타임에서 메인 모듈을 식별할 수 있을까요?

CommonJS

Node.js의 기본 모듈 방식인 CJS에선 아주 간단합니다. 공식 문서의 모듈 항목에선 다음과 같이 설명합니다.

Accessing the main module
When a file is run directly from Node.js, require.main is set to its module. That means that it is possible to determine whether a file has been run directly by testing require.main === module.
When the entry point is not a CommonJS module, require.main is undefined, and the main module is out of reach.

위 설명처럼 파일이 Node.js에서 직접 실행되면 require.mainmodule로 설정되기 때문에 require.main === module로 메인 모듈인지 아닌지 식별할 수 있습니다.

아까 살펴본 파이썬 예제를 타입스크립트로 변환해보겠습니다.

// greeting.ts
export function greeting() {
  console.log("Hello, World!");
}

// 메인 모듈일 경우 로직
if(require.main === module) {
  console.log("Hi, I am greeting module");
}
// app.ts
import { greeting } from './greeting';

greeting();
$ ts-node greeting.ts
Hi, I am greeting module

$ ts-node app.ts
Hello, World!

ES Modules

아시다시피 ESM 환경에선 module, require, exports, __dirname, __filename 등을 사용할 수 없습니다.
(일부는 유틸리티 함수로 만들어 쓸 수 있긴 합니다.)

때문에 ESM 환경에서는 실행된 프로세스의 파일 경로를 직접 확인해 메인 모듈을 식별해야 합니다.
이 때 import.metaprocess.argv를 활용하면 됩니다.

import.meta

import.meta는 ESM 환경에서 모듈의 참조를 얻을 수 있는 특별한 객체입니다.
import.meta.url을 통해 현재 파일의 경로를 알 수 있습니다.

// print.ts
export function printMetaUrl() {
  console.log(import.meta.url); 
}

// app.ts
import { printMetaUrl } from "./print.ts"
printMetaUrl(); // file:///Users/verycosy/example/print.ts

// index.ts
import { printMetaUrl } from "./print.ts"
printMetaUrl(); // file:///Users/verycosy/example/print.ts

어떤 모듈에서 printMetaUrl을 호출하더라도 출력되는 결과는 print.ts 파일의 경로입니다.
이 점을 이용하면 CJS 환경에서 사용할 수 있는 __filename도 다음 코드로 만들어낼 수 있습니다.

import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
console.log(__filename); // /Users/verycosy/example/print.ts

process.argv

process.argv는 프로세스가 실행될 때 전달된 인자들의 배열입니다.
다음과 같이 Node.js 프로세스를 실행시킨 뒤 process.argv의 값을 확인해보겠습니다.

node index.js one two=three four
console.log(process.argv);

[
  '/Users/verycosy/.nvm/versions/node/v20.9.0/bin/node', // node.js 바이너리 경로
  '/Users/verycosy/example/index.js', // 메인 모듈 경로
  // 전달된 값들
  'one',
  'two=three',
  'four'
]

보시다시피 process.argv의 1번 요소가 메인 모듈의 경로를 나타내기 때문에 위에서 만든 __filename과 비교하면 메인 모듈인지 아닌지 판단할 수 있습니다.

import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
if (__filename === process.argv[1]) {
  // 메인 모듈일 경우 로직
}

아래처럼 별도의 모듈로 빼낼 수도 있습니다.

// isMainModule.ts
import { fileURLToPath } from "url";

export function isMainModule(metaUrl: string) {
  return fileURLToPath(metaUrl) === process.argv[1];
}
// greeting.ts
import { isMainModule } from "./isMainModule.ts"

export function greeting() {
  console.log("Hello, World!");
}

// 메인 모듈일 경우 로직
if(isMainModule(import.meta.url)) {
  console.log("Hi, I am greeting module");
}

활용 사례

어떤 상황에서 유용할까요? 가장 쉽고 대표적인 사례는 모듈을 간단하게 테스트해보는 상황일 것입니다.
아까 작성해본 예제가 바로 그런 상황입니다. greeting 모듈을 다른 곳에서 사용하기 전에 정상적으로 작동하는지 간단히 확인해보았습니다.

하지만 실무에서 권장되는 상황은 아닙니다. 모듈이 잘 작동하는지 확인하기 위해선 테스트 코드를 작성하는 편이 더 좋습니다.

저는 DB의 seed 데이터를 관리할 때 유용하게 사용하고 있습니다.
다음과 같은 seed 함수가 있다고 가정해보겠습니다.

// seed.ts
export async function seed(db: DB) {
  // DB에 seed 데이터를 삽입한다
  ...
}

통합 테스트에서 seed 데이터를 미리 삽입해두는 경우, 테스트 코드를 아래와 같이 작성할 수 있습니다.

// sample.spec.ts
import { seed } from './seed';

describe('Sample', () => {
  beforeEach(async () => {
    const db = createDB();
    await seed(db);
  });

  ...
})

그리고 개발 환경에서 편의를 위해 동일한 seed 데이터를 활용할 수 있습니다.
seeding을 위한 스크립트를 따로 작성해보겠습니다.

// package.json
{
  "scripts": {
    "dev": "...",
    "db:seed": "ts-node seed.ts",
  },
  ...
}

npm run db:seed를 실행해도 seed 데이터는 삽입되지 않습니다.
seed.ts에는 seed 함수의 선언만 있을 뿐, 함수를 호출하고 있진 않기 때문입니다.
이를 해결하기 위해 seed.ts에 호출하는 코드를 작성한다면 테스트 코드에서는 seed 함수가 중복 호출되는 문제가 발생합니다.

이때 해결책은 2가지 입니다.

  1. db:seed 스크립트에서 사용할 seed 함수 호출 코드를 별도의 파일로 분리하기
  2. seed.ts 파일 안에서 메인 모듈 여부 판단하기

첫 번째 해결책부터 살펴보겠습니다.

// seedScript.ts
import { seed } from './seed';

seed(db).then(() => {
  console.log("Seeding Success")
}).catch(() => {
  console.log("Seeding Failure")
})

// package.json
{
  "scripts": {
    "db:seed": "ts-node seedScript.ts",
  },
}

아주 직관적입니다. 하지만 단순히 함수를 호출할 뿐인 파일을 추가로 관리해줘야 하는 단점이 있습니다.

두 번째 해결책은 아까 알아봤던 메인 모듈 식별 방법으로 조건문을 작성하는 방법입니다.

// seed.ts
export function seed(db: DB) {
  ...
}

if(require.main === module) {
  seed(db).then(() => {
    console.log("Seeding Success")
  }).catch(() => {
    console.log("Seeding Failure")
  })
}

이처럼 조건문으로 seed 함수의 호출 여부를 결정할 수 있습니다.

이밖의 다른 좋은 사례가 있다면 소개 부탁드리겠습니다 :)

돌아가기
© 2024 VERYCOSY.