Skip to main content

Electron에서의 ES 모듈 (ESM)

소개

ECMAScript 모듈(ESM) 형식은 JavaScript 패키지를 로드하는 표준 방식이다.

Chromium과 Node.js는 각각 ESM 사양을 구현하고 있으며, Electron은 상황에 따라 사용할 모듈 로더를 선택한다.

이 문서는 Electron에서의 ESM의 한계와, Electron, Node.js, Chromium에서의 ESM의 차이점을 설명한다.

info

이 기능은 electron@28.0.0 버전에서 추가되었다.

요약: ESM 지원 현황

이 표는 ESM이 지원되는 환경과 사용되는 ESM 로더를 개괄적으로 보여준다.

프로세스ESM 로더프리로드 시 ESM 로더적용 요구 사항
메인Node.jsN/A
렌더러 (샌드박스)Chromium지원되지 않음
렌더러 (비샌드박스 & 컨텍스트 격리)ChromiumNode.js
렌더러 (비샌드박스 & 비컨텍스트 격리)ChromiumNode.js

메인 프로세스

Electron의 메인 프로세스는 Node.js 환경에서 실행되며, Node.js의 ESM 로더를 사용한다. 사용 방법은 Node.js의 ESM 문서를 따르면 된다. 메인 프로세스에서 ESM을 활성화하려면 다음 조건 중 하나를 만족해야 한다:

  • 파일 확장자가 .mjs로 끝나야 한다.
  • 가장 가까운 상위 package.json 파일에 "type": "module"이 설정되어 있어야 한다.

자세한 내용은 Node.js의 모듈 시스템 결정 문서를 참고한다.

주의 사항

앱의 ready 이벤트 전에 await를 충분히 사용해야 한다

ES 모듈은 비동기적으로 로드된다. 이는 메인 프로세스 진입점에서 가져온 부수 효과만이 ready 이벤트 전에 실행됨을 의미한다.

이것은 일부 Electron API (예: app.setPath)가 앱의 ready 이벤트가 발생하기 전에 호출되어야 하기 때문에 중요하다.

Node.js ESM에서 최상위 await를 사용할 수 있으므로, ready 이벤트 전에 실행해야 하는 모든 Promise에 대해 await를 사용해야 한다. 그렇지 않으면 코드가 실행되기 전에 앱이 ready 상태가 될 수 있다.

이 점은 특히 동적 ESM import 문(정적 import는 영향을 받지 않음)을 사용할 때 유의해야 한다. 예를 들어, index.mjs가 최상위에서 import('./set-up-paths.mjs')를 호출하면, 동적 import가 해결될 때쯤이면 앱은 이미 ready 상태일 가능성이 높다.

index.mjs (Main Process)
// 여기에 await를 추가하여 `ready` 이전에 경로 설정이 완료되도록 보장한다
import('./set-up-paths.mjs')

app.whenReady().then(() => {
console.log('이 코드는 위의 import보다 먼저 실행될 수 있다')
})
트랜스파일러 변환

JavaScript 트랜스파일러(예: Babel, TypeScript)는 Node.js가 ESM import를 지원하기 전에 ES 모듈 구문을 지원하기 위해 이러한 호출을 CommonJS require 호출로 변환해왔다.

예제: @babel/plugin-transform-modules-commonjs

@babel/plugin-transform-modules-commonjs 플러그인은 ESM import를 require 호출로 변환한다. 정확한 구문은 importInterop 설정에 따라 다르다.

@babel/plugin-transform-modules-commonjs
import foo from "foo";
import { bar } from "bar";
foo;
bar;

// "importInterop: node" 설정으로 컴파일하면 ...

"use strict";

var _foo = require("foo");
var _bar = require("bar");

_foo;
_bar.bar;

이러한 CommonJS 호출은 모듈 코드를 동기적으로 로드한다. 트랜스파일된 CJS 코드를 네이티브 ESM으로 마이그레이션할 때, CJS와 ESM 간의 타이밍 차이에 주의해야 한다.

렌더러 프로세스

Electron의 렌더러 프로세스는 Chromium 환경에서 실행되며 Chromium의 ESM 로더를 사용한다. 이는 다음과 같은 의미를 가진다:

  • import 문은 Node.js 내장 모듈에 접근할 수 없다.
  • node_modules에서 npm 패키지를 불러올 수 없다.
<script type="module">
import { exists } from 'node:fs' // ❌ 동작하지 않음!
</script>

렌더러 프로세스에서 npm을 통해 JavaScript 패키지를 직접 불러오려면, webpack이나 Vite 같은 번들러를 사용해 클라이언트 측에서 사용할 수 있도록 코드를 컴파일하는 것을 권장한다.

프리로드 스크립트

렌더러의 프리로드 스크립트는 Node.js의 ESM 로더를 사용할 수 있을 때 활용한다. ESM의 사용 가능 여부는 렌더러의 sandboxcontextIsolation 설정 값에 따라 달라진다. 또한 ESM 로딩이 비동기적으로 이루어지기 때문에 몇 가지 주의사항이 있다.

주의사항

ESM 프리로드 스크립트는 .mjs 확장자를 사용해야 함

프리로드 스크립트는 "type": "module" 필드를 무시하므로, ESM 프리로드 스크립트를 작성할 때 반드시 .mjs 파일 확장자를 사용해야 한다.

샌드박스된 프리로드 스크립트는 ESM import를 사용할 수 없다

샌드박스된 프리로드 스크립트는 ESM 컨텍스트 없이 일반 자바스크립트로 실행된다. 외부 모듈을 사용해야 한다면, 프리로드 코드에 번들러를 사용하는 것을 권장한다. electron API는 여전히 require('electron')을 통해 로드한다.

샌드박스에 대한 더 자세한 정보는 프로세스 샌드박싱 문서를 참고한다.

콘텐츠가 없는 페이지에서 Unsandboxed ESM 프리로드 스크립트는 페이지 로드 후 실행됨

렌더러가 로드한 페이지의 응답 본문이 완전히 비어 있는 경우(예: Content-Length: 0), 프리로드 스크립트는 페이지 로드를 블로킹하지 않는다. 이로 인해 경합 조건이 발생할 수 있다.

이 문제가 영향을 미친다면, 응답 본문에 최소한의 내용을 포함하도록 변경한다(예: 빈 html 태그(<html></html>)). 또는 페이지 로드를 블로킹하는 CommonJS 프리로드 스크립트(.js 또는 .cjs)로 다시 전환한다.

ESM 프리로드 스크립트는 Node.js ESM 동적 임포트를 사용하려면 컨텍스트 격리가 필요하다

sandbox를 사용하지 않는 렌더러 프로세스에서 contextIsolation 플래그가 활성화되지 않은 경우, Node의 ESM 로더를 통해 파일을 동적으로 import()할 수 없다.

preload.mjs
// ❌ 컨텍스트 격리가 없으면 이 코드는 작동하지 않음
const fs = await import('node:fs')
await import('./foo')

이는 Chromium의 동적 ESM import() 함수가 렌더러 프로세스에서 일반적으로 우선권을 가지기 때문이다. 컨텍스트 격리가 없으면 동적 임포트 문에서 Node.js가 사용 가능한지 확인할 방법이 없다. 컨텍스트 격리를 활성화하면, 렌더러의 격리된 프리로드 컨텍스트에서 import() 문을 Node.js 모듈 로더로 라우팅할 수 있다.