import

import (모듈 가져오기)

정적 import 선언은 다른 모듈이 내보낸(export) 읽기 전용 라이브 바인딩(live binding)을 현재 모듈로 가져오는 데 사용합니다. 원문은 MDN의 JavaScript 참고 문서이며, 아래에서 상세 내용을 한국어로 번역해 설명합니다.

출처: import - JavaScript | MDN

본문

정적 import 선언은 다른 모듈이 내보낸 읽기 전용 라이브 바인딩을 가져오는 데 사용됩니다. 가져온 바인딩은 "라이브 바인딩"이라고 부르는데, 그 이유는 이 값이 해당 바인딩을 내보낸 모듈에 의해 갱신될 수는 있지만, 가져온 모듈 쪽에서는 재할당할 수 없기 때문입니다.

소스 파일에서 import 선언을 사용하려면 해당 파일이 런타임에 모듈(module)로 해석되어야 합니다. HTML에서는 <script> 태그에 type="module"을 추가하는 방식으로 이를 설정하며, 모듈은 자동으로 strict mode(엄격 모드)로 해석됩니다. 또한 함수처럼 생긴 동적 import()도 있는데, 이는 type="module" 스크립트가 아니어도 사용할 수 있습니다.

문법 (Syntax)

import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { default as alias } from "module-name";
import { export1, export2 } from "module-name";
import { export1, export2 as alias2, /* … */ } from "module-name";
import { "string name" as alias } from "module-name";
import defaultExport, { export1, /* … */ } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
  • defaultExport: 모듈의 기본(default) 내보내기를 가리키게 될 이름입니다. 유효한 JavaScript 식별자여야 합니다.
  • module-name: 가져올 모듈입니다. 작은따옴표 또는 큰따옴표로 감싼 문자열 리터럴만 허용됩니다. 지정자(specifier)의 평가 방식은 호스트 환경에 따라 정의됩니다. 대부분의 호스트는 브라우저와 동일하게 동작하며 지정자를 현재 모듈 URL(참고: import.meta.url)에 대해 상대적인 URL로 해석합니다. Node, 번들러 등 브라우저가 아닌 환경은 이 위에 자체적인 기능을 정의하는 경우가 많으므로 정확한 규칙은 해당 환경의 문서를 확인해야 합니다.
  • name: 가져온 것들을 참조할 때 일종의 네임스페이스(namespace)로 사용될 모듈 객체의 이름입니다. 유효한 JavaScript 식별자여야 합니다.
  • exportN: 가져올 내보내기의 이름입니다. module-name이 무엇을 내보내는지에 따라 식별자이거나 문자열 리터럴일 수 있습니다. 문자열 리터럴이라면 반드시 유효한 식별자로 별칭(alias)을 붙여야 합니다.
  • aliasN: 명명된(named) 가져오기를 가리키게 될 이름입니다. 유효한 JavaScript 식별자여야 합니다.

"module-name" 뒤에는 with 키워드로 시작하는 일련의 import attributes(가져오기 속성)가 올 수 있습니다.

설명 (Description)

import 선언은 모듈 안에서만, 그리고 항상 최상위(top-level)에서만 존재할 수 있습니다(즉 블록이나 함수 내부에는 올 수 없습니다). 모듈이 아닌 컨텍스트(예: type="module"이 없는 <script> 태그, eval, new Function 등 — 이들은 모두 파싱 목표가 "script"나 "function body"입니다)에서 import 선언을 만나면 SyntaxError가 발생합니다. 모듈이 아닌 컨텍스트에서 모듈을 로드하려면 동적 import() 문법을 사용해야 합니다.

모든 가져온 바인딩은 let, const, class, function, var, 다른 import 선언을 포함한 어떤 다른 선언과도 같은 스코프에 있을 수 없습니다.

import 선언은 문법적으로 매우 엄격하게 설계되어 있습니다(예: 문자열 리터럴 지정자만 허용, 최상위에서만 허용, 모든 바인딩이 식별자여야 함). 이 덕분에 모듈은 평가(evaluation)되기 전에 정적으로 분석되고 링크될 수 있습니다. 이는 모듈을 본질적으로 비동기로 만들고 top-level await 같은 기능을 가능하게 하는 핵심 요소입니다.

import 키워드 뒤에 "위상 수정자(phase modifier)"가 올 수 있어 모듈 가져오기 과정을 특정 단계에서 멈출 수 있습니다.

import defer
import source

이 각각의 문법은 서로 구별되는 선언 유형으로 취급됩니다.

import 선언의 형태 (Forms of import declarations)

네 가지 형태가 있습니다.

  • 명명된 가져오기 (Named import): import { export1, export2 } from "module-name";
  • 기본 가져오기 (Default import): import defaultExport from "module-name";
  • 네임스페이스 가져오기 (Namespace import): import * as name from "module-name";
  • 부수 효과 가져오기 (Side effect import): import "module-name";

명명된 가져오기(Named import). my-module 모듈에서 내보낸 myExport라는 값이 있으면, 아래처럼 현재 스코프에 삽입합니다. 여러 이름을 같은 모듈에서 가져올 수도 있고, 이름을 바꿔 가져올 수도 있습니다.

import { myExport } from "/modules/my-module.js";
import { foo, bar } from "/modules/my-module.js";
import { reallyReallyLongModuleExportName as shortName } from "/modules/my-module.js";

또한 모듈이 유효한 식별자가 아닌 문자열 리터럴로 멤버를 내보낼 수도 있는데, 이 경우 현재 모듈에서 사용하려면 반드시 별칭을 붙여야 합니다.

// /modules/my-module.js
const a = 1;
export { a as "a-b" };
import { "a-b" as a } from "/modules/my-module.js";

참고: import { x, y } from "mod"import defaultExport from "mod" 후에 defaultExport에서 x, y를 구조 분해(destructure)하는 것과 동등하지 않습니다. 명명된 가져오기와 기본 가져오기는 JavaScript 모듈에서 서로 구별되는 문법입니다.

기본 가져오기(Default import). 기본 내보내기는 대응하는 기본 import 문법으로 가져와야 합니다. 기본 내보내기는 이름을 명시하지 않기 때문에 원하는 어떤 식별자 이름이든 사용할 수 있습니다. 네임스페이스 가져오기나 명명된 가져오기와 함께 지정할 수도 있는데, 이 경우 기본 가져오기가 반드시 먼저 선언되어야 합니다.

import myDefault from "/modules/my-module.js";
import myDefault, * as myModule from "/modules/my-module.js";
import myDefault, { foo, bar } from "/modules/my-module.js";
import { default as myDefault } from "/modules/my-module.js";

default라는 이름을 가져오는 것은 기본 가져오기와 같은 효과를 내지만, default는 예약어이므로 반드시 별칭을 붙여야 합니다.

네임스페이스 가져오기(Namespace import). 아래 코드는 /modules/my-module.js에 있는 모듈의 모든 내보내기를 담은 myModule을 현재 스코프에 삽입합니다. myModule은 모든 내보내기를 속성으로 담은 네임스페이스 객체이며, null 프로토타입을 가진 밀봉(sealed) 객체입니다. 기본 내보내기는 default라는 키로 접근할 수 있습니다.

import * as myModule from "/modules/my-module.js";
myModule.doAllTheAmazingThings();

참고: JavaScript에는 이름 충돌 가능성이 높기 때문에 import * from "module-name" 같은 와일드카드 가져오기는 존재하지 않습니다.

부수 효과만을 위한 가져오기. 아무것도 가져오지 않고 모듈의 전역 코드를 실행하기 위해 사용합니다. 폴리필(polyfill)처럼 전역 변수를 변경하는 용도로 자주 쓰입니다.

import "/modules/my-module.js";

호이스팅(Hoisting). import 선언은 호이스팅됩니다. 즉, 가져온 식별자는 모듈 전체 스코프에서 사용할 수 있고, 그 부수 효과는 모듈의 나머지 코드가 실행되기 전에 발생합니다.

모듈 지정자 해석 (Module specifier resolution)

ECMAScript 사양은 모듈 지정자를 어떻게 해석할지는 정의하지 않고 호스트 환경(예: 브라우저, Node.js, Deno)에 맡깁니다. 브라우저 동작은 HTML 사양이 정의하며, 이것이 모든 환경의 사실상(de facto) 기준이 되었습니다.

일반적으로 인정되는 세 가지 지정자 유형이 있습니다.

  • /, ./, ../로 시작하는 상대 지정자(relative specifier): 현재 모듈 URL에 대해 상대적으로 해석됩니다.
  • 구문 분석 가능한 URL인 절대 지정자(absolute specifier): 그대로 해석됩니다.
  • 위에 해당하지 않는 베어 지정자(bare specifier).

상대 지정자에 대한 가장 주목할 만한 주의점은, 특히 CommonJS 관례에 익숙한 사람들에게, 브라우저가 하나의 지정자가 여러 후보로 암묵적으로 해석되는 것을 금지한다는 것입니다. CommonJS에서는 main.jsutils/index.js가 있다면 아래 모두 utils/index.js의 "기본 내보내기"를 가져옵니다.

// main.js
const utils = require("./utils");          // "index.js" 파일명 생략
const utils = require("./utils/index");    // ".js" 확장자만 생략
const utils = require("./utils/index.js"); // 가장 명시적인 형태

웹에서는 import x from "./utils"라고 쓰면 브라우저가 임포트 가능한 모듈을 찾을 때까지 utils, utils/index.js, utils.js, 그리고 잠재적으로 더 많은 URL에 요청을 보내야 하므로 비용이 큽니다. 따라서 HTML 사양에서는 지정자가 기본적으로 현재 모듈 URL에 대해 상대적으로 해석되는 URL이어야 하고, 파일 확장자나 index.js 파일명을 생략할 수 없습니다. 이 동작은 Node의 ESM 구현에도 계승되었지만 ECMAScript 사양의 일부는 아닙니다. 물론 서버가 해당 URL에 올바른 내용으로 응답한다면 import x from "./utils"가 실제로 동작할 수도 있습니다(다만 확장자 없는 요청은 보통 HTML 파일 요청으로 해석되므로 서버 쪽에 맞춤 해석 로직이 필요합니다).

절대 지정자는 임포트 가능한 소스 코드로 해석되는 어떤 종류의 URL이든 될 수 있습니다. HTTP URL은 웹에서 항상 지원되며, file: URL은 Node 같은 많은 비브라우저 런타임에서 지원되지만 보안상의 이유로 브라우저는 지원하지 않습니다. 데이터 URL은 브라우저·Node·Deno 등 많은 런타임이 지원하며, 작은 모듈을 소스 코드에 직접 내장할 때 유용합니다. 지원되는 MIME 타입은 JavaScript용 text/javascript, JSON 모듈용 application/json, WebAssembly 모듈용 application/wasm 등 임포트 가능한 소스 코드를 나타내는 것들입니다. node: URL은 Node 내장 모듈로 해석됩니다.

// HTTP URLs
import x from "https://example.com/x.js";
// Data URLs
import x from "data:text/javascript,export default 42;";
// Data URLs for JSON modules
import x from 'data:application/json,{"foo":42}' with { type: "json" };

참고로 text/javascript 데이터 URL은 여전히 모듈로 해석되지만, data: URL 스킴이 계층적(hierarchical)이지 않기 때문에 상대 import는 사용할 수 없습니다. 즉 import x from "data:text/javascript,import y from './y.js';"는 상대 지정자 './y.js'를 해석할 수 없어 오류를 던집니다.

베어 지정자는 CommonJS가 유행시킨 방식으로 node_modules 디렉터리 안에서 해석됩니다. 예를 들어 import x from "foo"라면 런타임은 현재 모듈의 상위 디렉터리들에 있는 어떤 node_modules 디렉터리에서든 foo 패키지를 찾습니다. 이 동작은 import maps를 사용해 브라우저에서 재현할 수 있으며, import maps를 통해 해석 방식을 더욱 사용자화할 수도 있습니다. 모듈 해석 알고리즘은 HTML 사양이 정의하는 import.meta.resolve 함수를 이용해 프로그래밍 방식으로도 실행할 수 있습니다.

예제 (Examples)

표준 import. 재사용 가능한 모듈을 만들어 지정된 범위 안의 모든 소수를 반환하는 함수를 내보냅니다.

// getPrimes.js
export function getPrimes(max) {
  const isPrime = Array.from({ length: max }, () => true);
  isPrime[0] = isPrime[1] = false;
  isPrime[2] = true;
  for (let i = 2; i * i < max; i++) {
    if (isPrime[i]) {
      for (let j = i ** 2; j < max; j += i) {
        isPrime[j] = false;
      }
    }
  }
  return [...isPrime.entries()].filter(([, isPrime]) => isPrime).map(([number]) => number);
}
import { getPrimes } from "/modules/getPrimes.js";
console.log(getPrimes(10)); // [2, 3, 5, 7]

가져온 값은 내보낸 쪽만 수정할 수 있습니다. 가져온 식별자는 라이브 바인딩이므로, 내보내는 모듈이 재할당하면 가져온 값도 변경되지만 가져온 모듈은 재할당할 수 없습니다. 다만 내보낸 객체를 보유한 모듈은 객체를 변형할 수 있고, 그 변형된 값은 같은 값을 가져온 모든 모듈이 관찰할 수 있습니다. 모듈 네임스페이스 객체를 통해서도 새 값을 관찰할 수 있습니다.

// my-module.js
export let myValue = 1;
setTimeout(() => { myValue = 2; }, 500);
// main.js
import { myValue } from "/modules/my-module.js";
import * as myModule from "/modules/my-module.js";
console.log(myValue);        // 1
console.log(myModule.myValue); // 1
setTimeout(() => {
  console.log(myValue);      // 2; my-module이 값을 갱신함
  console.log(myModule.myValue); // 2
  myValue = 3;               // TypeError: Assignment to constant variable.
}, 1000);

비-JavaScript 모듈 가져오기. JSON 모듈 등 비-JavaScript 모듈도 import 문으로 가져올 수 있지만, import attributes를 사용해 타입을 명시적으로 선언해야 합니다.

import data from "./data.json" with { type: "json" };

사양 및 호환성

import 선언의 문법은 ECMAScript® 2027 Language Specification의 # sec-imports 절에 정의되어 있습니다. 이 기능은 Baseline "Widely available"로 분류되며, 브라우저 전반에서 2018년 5월부터 사용 가능했습니다. 다만 기능의 일부 부분은 지원 수준이 각각 다를 수 있습니다.

더 알아보기