작성자 : Diogo Souza
원문 : https://blog.logrocket.com/how-to-use-ecmascript-modules-with-node-js/
Kevin Dangoor가 CommonJS 프로젝트를 시작한 후 2009년부터 어떻게 하면 자바스크립트가 웹 브라우저에서 실행되는 것 뿐만 아니라, 범위가 확대되어 백엔드 영역을 포함한 애플리케이션 개발에도 적합한지에 대해 토론이 시작되었다.
이 토론이 성공할 수 있었던 이유는 파이썬, 자바와 같은 언어의 표준 라이브러리를 따른 API 덕분이다. 오늘날 CommonJS로 인해 서버 사이드 애플리케이션용 자바스크립트, 커맨드 입력 도구, GUI 기반의 데스크톱 및 하이브리드 애플리케이션(Titanium, Adobe AIR 등) 등 많은 것들이 존재하게 되었다.
실제로 당신은 require()
를 사용할 때마다 CommonJS ES 모듈(역자주 : 이하 CJS)을 사용하거나 Node.js에 기본으로 포함된 ESM(역자주 : ECMAScript Module 약자, 이하 ES 모듈)을 사용하고 있다.
문제는 Node에서 이 두 ES 모듈을 함께 사용할 때이다. CommonJS는 이미 모듈이기 때문에 ESM과 같이 사용될 수 있는 좋은 방법을 찾아야만 한다. CommonJS는 동기식이지만 ESM이 비동기적으로 로드된다는 점을 제외하고 다른 부분들은 문제가 되지 않는다.
Babel 및 webpack과 같은 도구를 사용할 때는 로드가 동기 방식으로 처리된다. 그래서 브라우저와 네이티브 지원 없이 서버 사이드에서 모두 실행되는 애플리케이션을 위한 동일한 환경을 고려할 때 몇 가지 문제가 있을 것이다.
이 글에서는 브라우저와 서버 사이드와 같이 두 분야를 모두 지원할 수 있는 방법에 대해 알아보고, Node.js에서 얼마나 지원되는지 알아볼 예정이다. ESM을 사용하여 코드베이스를 마이그레이션하는 방법을 몇 가지 예제를 통해 살펴보겠다.
ES 모듈을 처음 사용하는 사람이라면, 먼저 모듈을 사용하는 방법을 살펴보자. React 또는 Vue.js를 사용했었다면 다음과 같은 코드를 보았을 것이다.
import React, {Fragment} from 'react';
// 또는
import Vue from './vue.mjs';
이 예제는 ES 모듈의 특성 중 기본 모듈(default module) 사용을 잘 보여주고 있다. 다음 코드를 고려해보자.
export default React;
기본 모듈은 한 개만 파일로 내보낼 수 있다. 그래서 첫 번째 예제에서 Fragment
는 { }
로 가져오는 것이다. Fragment
를 내보내는 방법은 다음과 같다.
export const Fragment = … ;
그리고 다음과 같이 당신의 모듈을 만들 수도 있다.
export const itsMine = 'It is my module';
이 코드를 mjs
확장자 파일로 저장하고, React 예제에서 본 것처럼 다른 파일에서 가져올 수 있다.
import { itsMine } from './myESTest.mjs'
alert(itsMine); // 'It is my module' 텍스트가 얼럿으로 뜰 것이다.
mjs
확장자는 js
파일들과 비교했을 때 혼란을 가져올 수 있다. 자바스크립트 명세에 따르면 mjs
와 js
사이에는 차이가 있다. 예를 들어서, 모듈은 정의상 엄격(use strict
와 같이)하다. 자바스크립트 모듈을 구현할 때 많은 체크 사항이 만들어지고 "안전하지 않은" 동작은 금지된다.
js
와 mjs
싸움은 모듈 또는 스크립트를 다루는 경우에 자바스크립트가 알아야 할 사실까지 이어지지만 제공되는 스펙은 따로 없다. 예를 들어서 CommonJS 스크립트를 가져오는 경우에는 'import from'
을 사용할 수 없다(require
만 된다). 그래서 각 확장자 파일이 적절한 파일을 가져오도록 강제할 수 있다.
mjs import from mjs
js require js
그렇다면 다음 시나리오에서는 어떤 일이 일어날까?
mjs import from js
js require mjs
ES 모듈은 정적(static)인 것으로 알려져 있다. 즉, 런타임이 아닌 컴파일 시간에만 "이동"할 수 있다. 이것이 파일 시작 부분에서 import
를 사용해 모듈을 가져오는 이유이다.
여기서 주목해야 할 첫 번째는 mjs
파일에서 require
를 사용할 수 없다는 것이다. 이전에 본 가져오기 구문(import
)을 사용해야만 한다.
import itsMine from './myESTest.js'
대신 기본 가져오기(module.exports
)가 CommonJS 파일(myESTest.js
)로 내보내진 경우에만 가능하다. 어떤가? 간단하지 않은가?
그러나 반대의 경우는 간단하지 않을 수 있다.
const itsMine require('./myESTest.mjs')
생각해보라. ES 모듈은 require
함수를 통해서 가져올 수 없다. 다른 방법으로 import from
구문을 사용해 시도한다면, CommonJS 파일은 import
를 사용할 수 없으므로 에러가 발생할 것이다.
import { itsMine } from './myESTest.mjs' // will err
Domenic Denicola는 다양한 방법으로 import()
함수를 통해 ES 모듈을 동적으로 가져오는 방식을 제안했다. 이 방식에 대해서 좀 더 알고 싶다면 위에 링크를 참조하라. 이 방식을 따르면 우리 코드는 다음과 같이 변경할 수 있다.
async function myFunc() {
const { itsMine } = await import('./myESTest.mjs')
}
myFunc()
그러나 주의할 점은 이 접근 방식은 async 함수를 사용할 수 있다는 것이다. async 함수 대신 여기에 기술된 콜백, 프라미스 등 다른 방식으로도 구현할 수 있다.
주의: 이 방식은 Node 10+부터 사용 가능하다.
여기 ES 모듈과 함께 Node.js를 실행하는 두 가지 방법이 있다.
--experimental-modules
플래그 사용 : 노드에서 제공하는 최소 기능이다.Node의 깃헙 리포지터리에서 "새 모듈 구현 계획" 페이지를 찾을 수 있다. 이 페이지에서 Node.js가 ECMAScript 모듈을 지원하는 공식 계획을 볼 수 있다. 계획은 4단계로 나누어져 있는데, 글을 쓰는 시점에는 --experimental-modules
플래그를 사용하지 않아도 될 정도의 희망과 함께 마지막 단계에 와 있다.
Node 환경에서 ES 모듈을 사용하기 위해 Node.js가 제공하는 첫 번째(공식적인) 방법부터 시작해보자.
이전에 언급했듯이 당신의 PC에 설치된 Node 버전이 10 이상인지 확인해야 한다. NVM의 힘을 빌려 현재 사용하고 있는 버전을 업그레이드하고 관리할 수 있다.
그런 다음 모듈이 어떻게 동작하는지 보여주기 위해 예제를 하나 만들 것이다. 다음 구조로 생성하라.
프로젝트 구조
첫 번째 파일인 hi.mjs
는 문자열 파라미터를 연결하여 메세지를 반환하는 단일 함수를 내보낸다.
// hi.mjs 코드
export function sayHi(name) {
return "Hi, " + name + "!"
}
export
기능을 사용한 것을 눈여겨보라. 두 번째 파일인 runner.mjs
는 이전에 만든 함수를 가져오고 콘솔에 메세지를 출력한다.
// runner.mjs 코드
import { sayHi } from './hi.mjs'
console.log(sayHi('LogRocket'))
다음 커맨드를 사용해 코드를 실행해본다.
node --experimental-modules runner.mjs
실행 결과는 다음과 같을 것이다.
테스트 결과
Node에서 실험적으로 사용하는 기능이라고 보여준다.
Babel, webpack 또는 우리가 원하는 곳에서 ES 모듈을 사용하는 데 도움이 되는 다른 도구들의 경우에는 더 간단한 해결 방법이 있다. @std/esm 패키지를 사용하는 것이다.
이 패키지는 기본적으로 Babel이나 다른 번들 도구를 분배하는 모듈 로더로 구성된다. 디펜던시를 필요로하지 않으며 Node.js 4 버전 이상에서 ES 모듈을 굉장히 빠르게 사용할 수 있다. 그리고 Node ESM 명세를 완벽하게 준수한다.
지금부터는 Express.js와 함께 웹 상에서의 다른 hello world
를 고려해보자. 우리는 CJS 파일을 만들어서 ESM과 대화할 것이다.
우선 프로젝트의 루트 폴더 안에서 다음 커맨드를 실행한다.
npm init -y
npm install --save @std/esm
npm install --save express
설치 단계를 따라하면서 package.json
를 설정한다. 끝나면 다음 두 개 파일을 생성한다.
runner.js
는 실행 시작점이면서 단일 자바스크립트 파일이다.hi-web.mjs
는 hello 함수에 접근하기 위한 Express 코드가 저장된다.hi-web.mjs
소스 코드를 작성해보자.
import express from "express";
import { sayHi } from "./hi.mjs";
const app = express();
app.get("/", (req, res) => res.json({ "message": sayHi("LogRocket") }));
app.listen(8080, () => console.log("Hello ESM with @std/esm !!"));
여기서는 이전에 작성했던 sayHi()
함수를 내보내는 mjs
파일을 사용하고 있다. 일단 다른 파일에서 mjs
를 완벽하게 가져올 수 있다는 사실을 알게 되는 건 그다지 놀라운 일이 아니다. 시작 스크립트로 이 파일을 가져오는 방법을 살펴보자.
// runner.js 코드
require = require("@std/esm")(module);
module.exports = require("./hi-web.mjs").default;
동적 가져오기를 사용하지 않는 경우에는 default
를 반드시 사용해야 한다. @std/esm은 require
를 재작성하고 사용 중인 Node 버전 모듈에 기능을 추가한다. 이것은 몇몇 인라인과 온 디맨드(on-demand) 변환, 실시간 처리 및 캐싱을 수행한다.
예제를 실행하기 전, 시작점이 될 파일을 package.json
스크립트에 추가한다.
...
"scripts": {
"start": "node runner.js"
},
npm start
커맨드를 실행하면 브라우저에 다음 결과가 나올 것이다.
브라우저 결과
ES 모듈이 Node로 동작하는 방식에 대해서 더 자세히 알고 싶다면 공식 문서를 확인해보라.
코드를 변환하려고 할 때 다음 중요한 사항들을 기억하라.
js
을 mjs
로 마이그레이션 할 때 기본 내보내기(module.exports
) 구문을 새로운 ESM export
구문으로 변경한다.require
는 각각의 가져오기 구문으로 변경되어야 한다.require
를 사용하려는 경우에는 await import
(또는 다른 방식으로 구현한 동적 import()
함수)를 통해 가져오기를 해야 하는 것을 기억하라. require
를 변경한다.mjs
파일을 사용할 때 올바른 미디어 유형(text/javascript
또는 application/javascript
)으로 배포해야 한다. 브라우저는 확장자를 신경 쓰지 않기 때문에 확장자가 필요한 것은 Node.js뿐이다. 이것이 Node.js가 파일이 CJS인지 아니면 ES 모듈인지 구분할 수 있는 방법이다.유익한 정보가 되었으면 좋겠다.