본문 바로가기
공부/express

[Express][Jest][ESModule] Cannot use 'import.meta' outside a module

by 웅대 2024. 6. 16.
728x90
반응형

상황

express 서버에서 ES Module을 사용하여 view router를 만들고 요청이 들어오면 html 파일을 보내주는 간단한 웹 서버를 구현하고 있었다.

 

원하는 html 파일을 보내주기 위해서 html 파일이 존재하는 위치를 가져와야 했다.

 

기본적으로 common JS를 사용할 때는 __dirname을 사용하여 파일의 위치를 알 수 있는데 ES Module에서는 __dirname을 사용할 수가 없다.

 

그래서 import.meta.url을 사용하여 아래와 같이 __dirname을 만들어서 사용하였다.

 

<__dirname 대체>

import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

 

<routes/views.js>

import express from 'express';
import path from 'path';
import { isAuthenticated, isNotAuthenticated } from '../middlewares/viewAuthMiddleware.js';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const router = express.Router();


router.get('/join', isNotAuthenticated, (req, res) => {
  res.sendFile(path.join(__dirname, '..', 'views', 'join.html'));
});

router.get('/login', isNotAuthenticated, (req, res) => {
  res.sendFile(path.join(__dirname, '..', 'views', 'login.html'));
});

router.get('/room', isAuthenticated, (req, res) => {
  res.sendFile(path.join(__dirname, '..', 'views', 'rooms.html'));
});

router.get('/room/:roomId', isAuthenticated, (req, res) => {
  res.sendFile(path.join(__dirname, '..', 'views', 'room.html'));
});

export default router;

 

__dirname을 대체하고 서버를 실행하면 이상 없이 잘 동작한다.

 

그런데 jest로 통합 테스트 코드를 작성할 때 문제가 발생하였다.

 

위 라우터는 서버에 등록하고 해당 서버에 supertest로 요청을 보내는 통합 테스트 코드를 작성하였는데 view router를 import 하는 과정에서 아래 오류가 발생하였다.

 

요약하자면 ES Module을 사용하여 jest 테스트 코드를 작성할 때 해당 코드에서 import.meta를 사용 중인 모듈을 import 할 때 에러가 발생한 것이다.

 

jest는 기본적으로 commonJS로 동작하기 때문에 ES Module로 사용하기 위해서는 두 가지 방법이 필요하다.

 

1. experimental support for ECMAScript Modules (ESM)

 

jest를 실행할 때 옵션을 주는 방법이다.

 

transforms: {} 처럼 변환해주는 코드를 제거하고 jest를 실행할 때 아래 둘 중 하나의 방식을 적용한다.

 

node --experimental-vm-modules node_modules/jest/bin/jest.js
NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" npx jest

 

자세한 설명은 jest의 공식 문서를 읽는 것을 추천한다.

https://jestjs.io/docs/ecmascript-modules

 

ECMAScript Modules · Jest

Jest ships with experimental support for ECMAScript Modules (ESM).

jestjs.io

 

2. 바벨 

바벨은 쉽게 말하면 표준이 아닌 자바스크립트를 표준 자바스크립트로 바꾸어주는 javascript transfiler이다.

 

ES Module은 표준이 아니기 때문에 바로 사용할 수 없고 바벨과 같은 javascript transfiler가 변환을 해줘야 한다.

 

즉 추가적인 바벨 설정을 통해 Node.js가 ES Module을 사용할 수 있다.

 

이 과정은 아래 블로그에 자세하게 설명이 되어 있어서 링크를 가져왔다.

https://poiemaweb.com/jest-esm

 

Jest에서 import/export를 사용하기 | PoiemaWeb

Jest에서 import/export를 사용하기

poiemaweb.com

 

나는 위 두 가지 방법 중에 두 번째 방법인 바벨을 적용했었다.

 

바벨을 적용해서 jest가 오류없이 돌아갔지만 여전히 import.meta를 사용하는 모듈을 import 하는 테스트에서는 "Cannot use 'import.meta' outside a module" 에러가 발생하였다.

 

해결 과정

가장 먼저 구글링을 해보았는데 나와 같은 문제를 겪고 있는 사람들이 꽤 있는 것을 확인할 수 있었다.

 

스택 오버플로우에 다양한 해결 방법들을 찾을 수 있었다.

https://stackoverflow.com/questions/64961387/how-to-use-import-meta-when-testing-with-jest

 

How to Use `import.meta` When Testing With Jest

I am writing Node.js code (in TypeScript) using ESModules and I need access to __dirname. In order to access the ESM equivalent of __dirname in CommonJS, I call dirname(fileURLToPath(import.meta.ur...

stackoverflow.com

 

다양한 방법들 중 처음으로 시도한 방법은 바벨 플러그인을 설정하는 것이었다.

 

나는 jest가 ES Module을 사용할 수 있게 바벨 설정을 했기 때문에 바벨 플러그인 방식이 합리적인 선택이라고 생각했다.

 

import.meta를 사용할 수 있게 도와주는 바벨 플러그인이 있다고 해서 해당 플러그인을 설치해서 적용해보았는데 바벨에 대한 이해도가 낮아서인지 실패하였다.

 

그 외의 다양한 방법들을 시도해보았는데 모두 실패하였고 결국 import.meta를 아예 사용하지 않는 것으로 결정하였다.

 

내가 import.meta를 사용하는 이유는 현재 파일의 디렉토리 주소를 알고 싶었을 뿐어서 import.meta를 포기하고 다른 방식을 선택하였다.

 

그 방식은 그냥 commonJS의 __dirname을 그대로 사용하는 것이다.

 

바벨 플러그인 방식은 바벨에 대한 어느정도의 이해도를 요구하는데 이 방식은 굉장히 간단하게 파일의 디렉토리 주소를 구할 수 있다.

 

우리의 프로젝트가 ES Module을 사용한다고 commonJS 모듈을 아예 사용하지 못 하는 건 아니다.

 

commonJS로 작성된 모듈이라도 import를 통해 모듈을 가져올 수 있다.

 

단, named export가 아닌 default export인 commonJS 모듈만 import를 통해 가져올 수 있다.

 

그렇다면 이제 ES Module로 작성된 프로젝트에서 commonJS의 __dirname을 사용해보자.

 

ES Module에서 commonJS 모듈 사용하는 방법 

적용하기 전에 package.json에 "type"이 설정되어 있는지 확인하자.

 

type이 없으면 기본 값인 commonJS가 적용되어 있고 type이 module로 설정되어 있다면 ES Module이 적용되어 있다.

 

원래 commonJS 확장자는 cjs, ES Module의 확장자는 mjs인데 package.json의 type을 설정하면 js 확장자를 자연스럽게 type에 해당하는 확장자로 인식을 한다.

 

즉 type이 module로 설정되어 있다면 확장자 mjs를 쓰지 않고 js로 개발을 할 수 있다.

 

나는 type이 module로 설정되어 있어서 그냥 js를 입력하면 mjs로 인식하게 된다.

 

그런데 우리는 commonJS를 import해서 써야하기 때문에 commonJS 모듈임을 알려주기 위해 cjs라는 확장자를 써야한다.

 

현재 나는 /routes/view.js에서 __dirname을 사용하려고 한다.

 

그렇다면 같은 위치에서 /routes/dirname.cjs 파일을 생성하자. (확장자 주의)

 

그리고 여기서 단순히 default export로 __dirname을 export 해주면 된다.

 

</routes/dirname.cjs>

module.exports = {__dirname};

 

그리고 view.js에서는 관련 코드를 모두 제거하고 dirname.cjs를 import 해서 __dirname을 사용하면 된다.

 

<이전 /routes/view.js>

import { dirname } from 'path';
import { fileURLToPath } from 'url';
    
const __dirname = dirname(fileURLToPath(import.meta.url));

 

<수정한 /routes/view.js>

import dirname from './dirname.cjs';
const {__dirname} = dirname;

 

이 문제를 해결하기 위해 바벨 플러그인 관련해서 3일 정도 고생을 했는데 단순히 commonJS의 __dirname을 import 할 수 있다는 것이 조금 허무하긴 하다.

 

바벨에 대해 이해도가 깊어지고 나면 바벨 플러그인을 통해 같은 문제를 해결하는 것도 시도해봐야겠다.

728x90
반응형

댓글