nuxt logo

문서 번역(비공식)

Nuxt.js
Version:v3.17

ES 모듈

Nuxt는 네이티브 ES 모듈을 사용합니다.

이 가이드는 ES 모듈이 무엇인지, 그리고 Nuxt 앱(또는 상위 라이브러리)을 ESM과 호환되도록 만드는 방법을 설명합니다.

배경

CommonJS 모듈

CommonJS (CJS)는 Node.js에 의해 도입된 형식으로, 격리된 JavaScript 모듈 간에 기능을 공유할 수 있게 합니다 (자세히 읽기). 다음 구문에 익숙할 수 있습니다:

const a = require('./a')

module.exports.a = a

webpack 및 Rollup과 같은 번들러는 이 구문을 지원하며, 브라우저에서 CommonJS로 작성된 모듈을 사용할 수 있게 합니다.

ESM 구문

대부분의 경우, 사람들이 ESM과 CJS에 대해 이야기할 때, 모듈을 작성하는 다른 구문에 대해 이야기하고 있습니다.

import a from './a'

export { a }

ECMAScript 모듈(ESM)이 표준이 되기 전에(10년 이상 걸렸습니다!), webpack과 같은 도구 및 TypeScript와 같은 언어는 소위 ESM 구문을 지원하기 시작했습니다. 그러나 실제 사양과 몇 가지 주요 차이점이 있습니다. 여기에 유용한 설명서가 있습니다.

'네이티브' ESM이란 무엇인가요?

오랫동안 ESM 구문을 사용하여 앱을 작성해 왔을 수 있습니다. 결국, 브라우저에서 네이티브로 지원되며, Nuxt 2에서는 작성한 모든 코드를 적절한 형식(CJS는 서버용, ESM은 브라우저용)으로 컴파일했습니다.

패키지에 모듈을 추가할 때는 약간 다릅니다. 샘플 라이브러리는 CJS 및 ESM 버전을 모두 노출하고, 원하는 것을 선택할 수 있게 합니다:

{
  "name": "sample-library",
  "main": "dist/sample-library.cjs.js",
  "module": "dist/sample-library.esm.js"
}

따라서 Nuxt 2에서는 번들러(webpack)가 서버 빌드를 위해 CJS 파일('main')을 가져오고, 클라이언트 빌드를 위해 ESM 파일('module')을 사용했습니다.

그러나 최근 Node.js LTS 릴리스에서는 Node.js 내에서 네이티브 ESM 모듈을 사용할 수 있게 되었습니다. 이는 Node.js 자체가 ESM 구문을 사용하여 JavaScript를 처리할 수 있음을 의미하지만, 기본적으로 그렇게 하지는 않습니다. ESM 구문을 활성화하는 가장 일반적인 두 가지 방법은 다음과 같습니다:

  • package.json 내에 "type": "module"을 설정하고 .js 확장을 계속 사용
  • .mjs 파일 확장을 사용 (권장)

이것이 Nuxt Nitro에서 하는 방식입니다. 우리는 .output/server/index.mjs 파일을 출력합니다. 이는 Node.js에 이 파일을 네이티브 ES 모듈로 처리하도록 지시합니다.

Node.js 컨텍스트에서 유효한 임포트는 무엇인가요?

모듈을 require하는 대신 import할 때, Node.js는 이를 다르게 해석합니다. 예를 들어, sample-library를 임포트할 때, Node.js는 해당 라이브러리의 package.json에서 main이 아닌 exports 또는 module 항목을 찾습니다.

이는 const b = await import('sample-library')와 같은 동적 임포트에도 해당됩니다.

Node는 다음과 같은 종류의 임포트를 지원합니다 (자세한 내용은 문서를 참조하세요):

  1. .mjs로 끝나는 파일 - ESM 구문을 사용할 것으로 예상됩니다.
  2. .cjs로 끝나는 파일 - CJS 구문을 사용할 것으로 예상됩니다.
  3. .js로 끝나는 파일 - package.json"type": "module"이 없는 한 CJS 구문을 사용할 것으로 예상됩니다.

어떤 종류의 문제가 있을 수 있나요?

오랫동안 모듈 작성자들은 ESM 구문 빌드를 생성했지만, .esm.js 또는 .es.js와 같은 관례를 사용하여 이를 package.jsonmodule 필드에 추가했습니다. 이는 번들러(webpack과 같은)에서 파일 확장자에 특별히 신경 쓰지 않기 때문에 지금까지 문제가 되지 않았습니다.

그러나 Node.js ESM 컨텍스트에서 .esm.js 파일이 있는 패키지를 임포트하려고 하면 작동하지 않으며 다음과 같은 오류가 발생할 수 있습니다:

Terminal
(node:22145) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
/path/to/index.js:1

export default {}
^^^^^^

SyntaxError: Unexpected token 'export'
    at wrapSafe (internal/modules/cjs/loader.js:1001:16)
    at Module._compile (internal/modules/cjs/loader.js:1049:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    ....
    at async Object.loadESM (internal/process/esm_loader.js:68:5)

또한 Node.js가 CJS로 생각하는 ESM 구문 빌드에서 명명된 임포트를 할 때도 이 오류가 발생할 수 있습니다:

Terminal
file:///path/to/index.mjs:5
import { named } from 'sample-library'
         ^^^^^
SyntaxError: Named export 'named' not found. The requested module 'sample-library' is a CommonJS module, which may not support all module.exports as named exports.

CommonJS modules can always be imported via the default export, for example using:

import pkg from 'sample-library';
const { named } = pkg;

    at ModuleJob._instantiate (internal/modules/esm/module_job.js:120:21)
    at async ModuleJob.run (internal/modules/esm/module_job.js:165:5)
    at async Loader.import (internal/modules/esm/loader.js:177:24)
    at async Object.loadESM (internal/process/esm_loader.js:68:5)

ESM 문제 해결

이러한 오류가 발생하면, 문제는 거의 확실히 상위 라이브러리에 있습니다. 그들은 Node에 의해 임포트될 수 있도록 라이브러리를 수정해야 합니다.

라이브러리 트랜스파일링

그동안, Nuxt에 이러한 라이브러리를 임포트하지 않도록 build.transpile에 추가할 수 있습니다:

export default defineNuxtConfig({
  build: {
    transpile: ['sample-library']
  }
})

이 라이브러리에 의해 임포트되는 다른 패키지를 또한 추가해야 할 수도 있습니다.

라이브러리 별칭 설정

일부 경우에는 CJS 버전으로 라이브러리를 수동으로 별칭 설정해야 할 수도 있습니다. 예를 들어:

export default defineNuxtConfig({
  alias: {
    'sample-library': 'sample-library/dist/sample-library.cjs.js'
  }
})

기본 내보내기

CommonJS 형식의 종속성은 module.exports 또는 exports를 사용하여 기본 내보내기를 제공할 수 있습니다:

node_modules/cjs-pkg/index.js
module.exports = { test: 123 }
// 또는
exports.test = 123

이것은 일반적으로 이러한 종속성을 require할 때 잘 작동합니다:

test.cjs
const pkg = require('cjs-pkg')

console.log(pkg) // { test: 123 }

Node.js의 네이티브 ESM 모드, typescript의 esModuleInterop 활성화 및 webpack과 같은 번들러는 이러한 라이브러리를 기본적으로 임포트할 수 있도록 호환성 메커니즘을 제공합니다. 이 메커니즘은 종종 "interop require default"라고 불립니다:

import pkg from 'cjs-pkg'

console.log(pkg) // { test: 123 }

그러나 구문 감지의 복잡성과 다양한 번들 형식 때문에, interop 기본값이 실패하여 다음과 같은 결과가 발생할 가능성이 항상 있습니다:

import pkg from 'cjs-pkg'

console.log(pkg) // { default: { test: 123 } }

또한 동적 임포트 구문을 사용할 때(CJS 및 ESM 파일 모두에서), 항상 이러한 상황이 발생합니다:

import('cjs-pkg').then(console.log) // [Module: null prototype] { default: { test: '123' } }

이 경우, 기본 내보내기를 수동으로 interop해야 합니다:

// 정적 임포트
import { default as pkg } from 'cjs-pkg'

// 동적 임포트
import('cjs-pkg').then(m => m.default || m).then(console.log)

더 복잡한 상황을 처리하고 더 안전하게 하기 위해, 우리는 Nuxt에서 mlly를 사용하여 명명된 내보내기를 보존할 수 있습니다.

import { interopDefault } from 'mlly'

// 모양이 { default: { foo: 'bar' }, baz: 'qux' }라고 가정
import myModule from 'my-module'

console.log(interopDefault(myModule)) // { foo: 'bar', baz: 'qux' }

라이브러리 작성자 가이드

좋은 소식은 ESM 호환성 문제를 해결하는 것이 비교적 간단하다는 것입니다. 두 가지 주요 옵션이 있습니다:

  1. ESM 파일의 이름을 .mjs로 끝나도록 변경할 수 있습니다.

    이것이 권장되고 가장 간단한 접근 방식입니다. 라이브러리의 종속성과 빌드 시스템과 관련된 문제를 해결해야 할 수도 있지만, 대부분의 경우 이 방법이 문제를 해결할 것입니다. 또한 CJS 파일의 이름을 .cjs로 끝나도록 변경하는 것도 권장됩니다. 이는 명확성을 높이기 위함입니다.

  2. 라이브러리를 ESM 전용으로 만들 수 있습니다.

    이는 package.json"type": "module"을 설정하고, 빌드된 라이브러리가 ESM 구문을 사용하도록 보장하는 것을 의미합니다. 그러나 종속성과 관련된 문제가 발생할 수 있으며, 이 접근 방식은 라이브러리가 오직 ESM 컨텍스트에서만 소비될 수 있음을 의미합니다.

마이그레이션

CJS에서 ESM으로의 초기 단계는 require 사용을 import로 업데이트하는 것입니다:

module.exports = ...

exports.hello = ...
const myLib = require('my-lib')

ESM 모듈에서는 CJS와 달리 require, require.resolve, __filename__dirname 전역 변수가 제공되지 않으며, import()import.meta.filename으로 대체해야 합니다.

import { join } from 'path'

const newDir = join(__dirname, 'new-dir')
const someFile = require.resolve('./lib/foo.js')

모범 사례

  • 기본 내보내기보다는 명명된 내보내기를 선호하세요. 이는 CJS 충돌을 줄이는 데 도움이 됩니다. (자세한 내용은 기본 내보내기 섹션 참조)

  • 라이브러리를 브라우저 및 엣지 워커에서 Nitro 폴리필 없이 사용할 수 있도록 Node.js 내장 및 CommonJS 또는 Node.js 전용 종속성에 의존하지 않도록 하세요.

  • 조건부 내보내기가 있는 새로운 exports 필드를 사용하세요. (자세히 읽기).

{
  "exports": {
    ".": {
      "import": "./dist/mymodule.mjs"
    }
  }
}