ECMAScript 6 Module로 코드 캡슐화하기

ECMAScript 6 Module로 코드 캡슐화하기

JavaScript의 “모든 공유” 방식의 코드 로드는 JavaScript를 오류가 발생하기 쉬운 가장 혼란스러운 언어 중 하나로 만듭니다. 다른 언어에서는 패키지와 같은 개념을 사용하여 코드 범위를 정의하지만 ECMAScript 6 이전에는 응용 프로그램의 모든 JavaScript 파일에 정의된 모든 것이 하나의 전역 Scope을 공유했습니다. 웹 애플리케이션이 더욱 복잡해지고 JavaScript 코드가 더많이 사용됨에 따라 이러한 접근 방식은 이름 충돌 및 보안 문제와 같은 문제를 야기했습니다. ECMAScript 6의 한가지 목표는 Scope 문제를 해결하고 JavaScript 응용 프로그램에 순서를 지정하는 것이 었습니다. 이것이 Module이 도입된 이유입니다.

Module은 무엇일까요?

Module은 다른 모드로 로드되는 JavaScript 파일입니다 (Script는 JavaScript가 작동하는 원래 방식으로 로드 됨). ModuleScript와 매우 다른 의미를 가지고 있기 때문에 다른 모드가 필요합니다.

  1. Module 코드는 자동으로 strict 모드에서 실행되며 strict 모드를 거부 할 방법이 없습니다.
  2. Module의 최상위 레벨에서 작성된 변수는 공유된 전역 범위에 자동으로 추가되지 않습니다. Module의 최상위 범위에만 존재합니다.
  3. Module의 최상위 레벨에있는 this의 값은 `undefined ‘입니다.
  4. Module은 코드 내에서 HTML 스타일의 주석을 허용하지 않습니다 (JavaScript의 초기 브라우저 시절 남은 기능).
  5. ModuleModule 외부에서 사용할 수 있어야하는 모든 것을 export 해야합니다.
  6. Module은 다른 Module에서 바인딩을 가져올 수 있습니다.

이 차이는 언뜻보기에 작게 보일 수 있지만 JavaScript 코드가 로드되고 평가되는 방식에 중요한 변화를 나타냅니다. 이것은 이장 전체에서 논의 할 것입니다. Module의 진정한 힘은 파일의 모든 것보다는 필요한 바인딩만 내보내고 가져 오는 기능입니다. Modulescripts와 어떻게 다른지 이해하기 위해서는 내보내기(export)와 가져 오기(import)를 잘 이해해야합니다.

export 기초

export 키워드를 사용하여 코드의 일부를 다른 Module에 노출할 수 있습니다. 가장 단순한 경우에, 변수, 함수 또는 클래스 선언 앞에export를 두어 다음과 같이 Module에서 내보낼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// export data
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
// export function
export function sum(num1, num2) {
return num1 + num1;
}
// export class
export class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
}
// 이 함수는 Module에서 private입니다.
function subtract(num1, num2) {
return num1 - num2;
}
// 함수를 정의하고...
function multiply(num1, num2) {
return num1 * num2;
}
// ...나중에 내보낼수 있습니다.
export { multiply };

이 예에서 주의해야 할 몇가지 사항이 있습니다. 첫째, export 키워드를 제외하고 모든 선언은 다른 경우와 완전히 동일합니다. 내보낸 각 함수 또는 클래스에는 이름이 있습니다. 왜냐하면 내보낸 함수와 클래스 선언에는 이름이 필요하기 때문입니다. default 키워드를 사용하지 않는 이상 이구문을 사용하여 익명 함수나 클래스를 내보낼 수 없습니다 (“ModuleDefault값” 섹션 참조).

다음으로, 정의되었을 때 export하지 않은 multiply() 함수를 생각해 보겠습니다. 이는 항상 선언에서 export할 필요가 없기 때문에 효과적입니다. 참조를 export할 수도 있습니다. 마지막으로 이 예제에서는 subtract() 함수를 export하지 않습니다. 명시적으로 export하지 않은 변수, 함수 또는 클래스는 Module에 비공개로 남아 있기 때문에 이 함수는 이 Module 외부에서 액세스할 수 없습니다.

import 기초

export가 있는 Module이 있으면 import 키워드를 사용하여 다른 Module의 기능에 액세스할 수 있습니다. import 문의 두 부분은 가져올 식별자와 그 식별자를 가져올 Module입니다. 아래는 문장의 기본형태입니다.

1
import { identifier1, identifier2 } from "./example.js";

import 뒤의 중괄호는 주어진 Module로부터 가져올 바인딩을 나타냅니다. 키워드 from은 지정된 바인딩을 가져올 Module을 나타냅니다. Module은 경로를 나타내는 문자열로 지정됩니다 (Module 지정자라고 함). 브라우저는 <script> 요소에 전달할 동일한 경로 형식을 사용합니다. 즉, 파일 확장명을 포함해야합니다. 반대로, Node.js는 파일 시스템 접두사를 기반으로 로컬 파일과 패키지를 구별하는 전통적인 규칙을 따릅니다. 예를 들어 example은 패키지이고 ./example.js는 로컬 파일입니다.

import 할 바인딩 목록은 Destructured 객체와 유사하지만 보이지는 않습니다.

Module로부터 바인딩을 import할 때 바인딩은 const를 사용하여 정의된 것처럼 동작합니다. 즉, 동일한 이름의 다른 변수를 정의할 수 없으며 (같은 이름의 다른 바인딩을 가져 오는 것을 포함하여), import 문 앞에 식별자를 사용하거나 값을 변경할 수 없다는 뜻입니다.

단일 바인딩 import

“export 기초” 섹션의 첫 번째 예제가 파일 example.jsModule에 있다고 가정합니다. 여러 가지 방법으로 해당 Module에서 바인딩을 가져오고 사용할 수 있습니다. 예를 들어 하나의 식별자만 가져올 수 있습니다.

1
2
3
4
5
6
// 하나만 가져 오기
import { sum } from "./example.js";
console.log(sum(1, 2)); // 3
sum = 1; // error

example.js는 하나 이상의 함수를 export하지만, 이 예제는 sum()함수만 import합니다. sum에 새로운 값을 할당하려고하면, 가져온 바인딩을 재할당할 수 없기 때문에 결과는 오류입니다.

브라우저와 Node.js간에 최상의 호환성을 위해 import하는 파일의 시작 부분에 /, ./ 또는 ../을 포함시킵니다.

여러 바인딩 import

예제 Module에서 여러 바인딩을 import하기위해 다음과 같이 명시적으로 나열할 수 있습니다.

1
2
3
4
// import multiple
import { sum, multiply, magicNumber } from "./example.js";
console.log(sum(1, magicNumber)); // 8
console.log(multiply(1, 2)); // 2

여기 예제 Module에서 sum, multiply, magicNumber의 세 가지 바인딩을 import합니다. 그런 다음 로컬에 정의된 것처럼 사용합니다.

Module에서 모두 import

또한 Module의 전체를 단일 객체로 import할 수있는 특별한 경우가 있습니다. 그런 다음 해당 객체에서 export한 모든 것을 프로퍼티로로 사용할 수 있습니다.

1
2
3
4
5
// import everything
import * as example from "./example.js";
console.log(example.sum(1,
example.magicNumber)); // 8
console.log(example.multiply(1, 2)); // 2

이 코드에서,example.js에 있는 export한 모든 바인딩은 example이라는 객체에 로드됩니다. 그러면 이름이 지정된 export (sum() 함수, multiple() 함수 및 magicNumber)는 example의 프로퍼티로 액세스할 수 있습니다. 이 import 형식을 namespace import라고합니다. 왜냐하면 example 객체는 example.js 파일안에 존재하지 않고 example.jsexport한 모든 멤버들에 대한 네임 스페이스 객체로 사용되기 위해 생성되기 때문입니다.

그러나 import 문에서 Module을 몇번이나 사용하더라도 Module은 한번만 실행됩니다. Moduleimport한 코드가 실행된 후에, 인스턴스화된 Module은 메모리에 유지되고 다른 import 문이 그것을 참조할 때마다 재사용됩니다. 다음을 살펴보겠습니다.

1
2
3
import { sum } from "./example.js";
import { multiply } from "./example.js";
import { magicNumber } from "./example.js";

Module에는 import문이 세개 있지만 example.js는 한번만 실행됩니다. 동일한 애플리케이션의 다른 Moduleexample.js에서 바인딩을 import하는 경우 이 Module은 이 코드에서 사용하는 것과 동일한 Module 인스턴스를 사용합니다.

Module 구문 제한 사항

exportimport의 중요한 제한 사항은 구문과 함수 밖에서 사용해야한다는 것입니다. 예를 들어 이 코드는 구문 오류를 발생시킵니다.

1
2
3
if (flag) {
export flag; // syntax error
}

export 문장이 if문 안에 있습니다. 이것은 허용되지 않습니다. export는 어떤 방식으로든 조건부 이거나 동적일 수 없습니다. Module 구문이 존재하는 한가지 이유는 JavaScript 엔진이 export하는 것을 정적으로 결정하게 하기 위해서입니다. 따라서 Module의 최상위 레벨에서만 export를 사용할 수 있습니다.

마찬가지로 명령문 내부에서는 import를 사용할 수 없습니다. 최상위 레벨에서만 사용할 수 있습니다. 즉, 아래 코드는 구문 오류입니다.

1
2
3
function tryImport() {
import flag from "./example.js"; // syntax error
}

동적으로 바인딩을 export할 수 없는 것과 같은 이유로 동적으로 바인딩을 import할 수 없습니다. exportimport 키워드는 텍스트 편집기와 같은 도구가 Module에서 어떤 정보를 사용할 수 있는지 쉽게 알수있도록 정적으로 설계되었습니다.

import한 바인딩의 미묘한 특성

ECMAScript 6의 import 문은 일반 변수와 같이 원래 바인딩을 단순히 참조하는 것이 아니라 변수, 함수 및 클래스에 대한 읽기 전용 바인딩을 만듭니다. 바인딩을 import하는 Module은 값을 변경할 수 없지만 해당 식별자를 export하는 Module은 값을 변경할 수 있습니다. 예를 들어, 이 Module을 사용한다고 가정 해보겠습니다.

1
2
3
4
export var name = "Nicholas";
export function setName(newName) {
name = newName;
}

이 두 바인딩을 import하면 setName() 함수는 name의 값을 바꿀 수 있습니다.

1
2
3
4
5
6
7
import { name, setName } from "./example.js";
console.log(name); // "Nicholas"
setName("Greg");
console.log(name); // "Greg"
name = "Nicholas"; // error

setName("Greg")에 대한 호출은 setName()exportModule로 돌아가서 실행이 되고 name"Greg"로 설정합니다. 이 변경은 importname 바인딩에 자동으로 반영됩니다. nameexportname 식별자의 로컬 이름이기 때문입니다. 위의 코드에서 사용된 nameimportModule에서 사용된 name은 같지 않습니다.

이름을 변경하여 export와 import

때로는 Module에서 import한 변수, 함수 또는 클래스의 원래 이름을 사용하지 않을 수도 있습니다. 다행히도 exportimport하는 동안 export의 이름을 변경할 수 있습니다.

첫번째 경우, 다른 이름으로 export하려는 함수가 있다고 가정합니다. as 키워드를 사용하여 함수가 Module 외부로 알려진 이름을 지정할 수 있습니다.

1
2
3
4
5
function sum(num1, num2) {
return num1 + num2;
}
export { sum as add };

여기서 sum() 함수(sum은 로컬 이름입니다)는 add()(addexport한 이름입니다)로 export됩니다. 즉, 다른 Module이 이 함수를 import하기 원할 때 sum 대신 add라는 이름을 사용해야 합니다.

1
import { add } from "./example.js";

함수를 import하는 Module이 다른 이름을 사용하고 싶다면, as를 사용할 수도 있습니다.

1
2
3
import { add as sum } from "./example.js";
console.log(typeof add); // "undefined"
console.log(sum(1, 2)); // 3

이 코드는 import name을 사용하여 add() 함수를 import해서 sum()(로컬 이름)으로 이름을 바꿉니다. 즉, 이 Module에는 add라는 이름의 식별자가 없습니다.

Module의 Default 값

Module 구문은 실제로 Module에서 Default값을 export하고 import하는데 최적화되어 있습니다. 이 패턴은 CommonJS(브라우저 외부에서 JavaScript를 사용하기위한 또 다른 사양)와 같이 다른 Module 시스템에서 상당히 일반적이었습니다. ModuleDefault값은 default 키워드로 지정된 단일 변수, 함수 또는 클래스이며 Module 당 하나의 Default export만 설정할 수 있습니다. default 키워드를 다중 export와 함께 사용하는 것은 구문 오류입니다.

Default 값 export

다음은default 키워드를 사용하는 간단한 예제입니다.

1
2
3
export default function(num1, num2) {
return num1 + num2;
}

Module은 함수를 Default값으로 export합니다. default 키워드는 이것이 Default export 임을 나타냅니다. 이 함수는 Module 자체가 함수를 나타내기 때문에 이름을 요구하지 않습니다.

다음과 같이 export default 다음에 식별자를 배치하여 식별자를 Default export로 지정할 수도 있습니다.

1
2
3
4
5
function sum(num1, num2) {
return num1 + num2;
}
export default sum;

여기에서 sum() 함수가 먼저 정의되고 나중에 ModuleDefault값으로 export됩니다. Default값으로 계산을 사용해야하는 경우 이방법을 선택할 수 있습니다.

식별자를 Default export로 지정하는 세 번째 방법은 다음과 같이 이름 바꾸기 구문을 사용하는 것입니다.

1
2
3
4
5
function sum(num1, num2) {
return num1 + num2;
}
export { sum as default };

식별자 default는 이름 바꿔서 export에서 특별한 의미를 가지며 값이 ModuleDefault값이어야 함을 나타냅니다. default는 JavaScript에서 키워드이기 때문에 변수, 함수 또는 클래스 이름으로 사용할 수 없습니다 (프로퍼티 이름으로 사용할 수 있음). 그래서 export의 이름을 바꾸기 위해 default를 사용하는 것은 Default값이 아닌 export에 정의된 방법과 일관성을 유지하는 특별한 경우입니다. 이 구문은 단일 export 문을 사용하여 Default값을 포함한 여러 export를 동시에 지정하려는 경우 유용합니다.

Default 값 import

다음 구문을 사용하여 Module에서 Default값을 import할 수 있습니다.

1
2
3
4
// import the default
import sum from "./example.js";
console.log(sum(1, 2)); // 3

import 명령문은 Module example.js에서 Default값을 import합니다. Default가 아닌 import에서 볼 수있는 것과는 달리 중괄호는 사용되지 않습니다. 로컬 이름 sumModuleexport하는 Default 함수을 나타내는데 사용됩니다. 이 구문은 가장 깨끗하고, ECMAScript 6의 제작자는 이것이 웹상에서 가장 많이 사용되는 형식이기를 기대하며 이미 존재하는 객체를 사용할 수 있습니다.

Default 바인딩과 하나 이상의 Default가 아닌 바인딩을 모두 export하는 Module의 경우 하나의 명령문으로 모든 export 바인딩을 가져올 수 있습니다. 예를 들어, 이 Module을 가지고 있다고 가정해 보겠습니다.

1
2
3
4
5
export let color = "red";
export default function(num1, num2) {
return num1 + num2;
}

다음의 import 문을 사용하여colorDefault 함수를 모두 import할 수 있습니다.

1
2
3
4
import sum, { color } from "./example.js";
console.log(sum(1, 2)); // 3
console.log(color); // "red"

쉼표는 Default 로컬 이름을 중괄호로 묶인 Default값이 아닌 값과 구분합니다. Default값은 import 문에서 Default가 아닌값 앞에 와야한다는 것을 명심하십시오.

Default값을 export하는 것과 마찬가지로, 이름 바꾸기 구문을 이용하여 Default값을 import할 수 있습니다.

1
2
3
4
5
// 이전 예제와 동일합니다.
import { default as sum, color } from "example";
console.log(sum(1, 2)); // 3
console.log(color); // "red"

이 코드에서 Default export(default)는 sum으로 이름이 바뀌고 추가적으로 color export 또한 가져옵니다. 이 예제는 앞의 예제와 동일합니다.

바인딩을 다시 export

Moduleimport한 내용을 다시 export하고 싶을 때가 있을 수 있습니다 (예 : 여러 개의 작은 Module로 라이브러리를 만드는 경우). 이 장에서 이미 설명한 패턴을 사용하여 import한 값을 다음과 같이 다시 export할 수 있습니다.

1
2
import { sum } from "./example.js";
export { sum }

이게 효과가 있지만, 하나의 문장으로 똑같은 일을 할 수 있습니다.

1
export { sum } from "./example.js";

이 형태의 exportsum의 선언을 위해 지정된 Moduleexport합니다. 물론 동일한 값에 대해 다른 이름을 export하도록 선택할 수도 있습니다.

1
export { sum as add } from "./example.js";

여기서 sum"./example.js"에서 import한 후 add라는 이름으로 export합니다.

다른 Module의 모든 것을 export하려면 * 패턴을 사용할 수 있습니다.

1
export * from "./example.js";

모든 것을 export하면 이름이 있는 export뿐만 아니라 Default값도 포함되기 때문에 Module에서 export할 수있는 것에 영향을 줄 수 있습니다. 예를 들어, example.jsDefault export가 있는 경우 이 구문을 사용하면 새로운 Default export를 정의할 수 없습니다.

바인딩없이 import

일부 Module은 아무것도 export할 수 없으며 전역 범위의 객체만 수정하기도 합니다. Module 내의 최상위 변수, 함수 및 클래스가 전역 범위에서 자동으로 끝나지는 않지만 이것은 Module이 전역 범위에 액세스 할 수 없다는 것을 의미하지는 않습니다. ArrayObject와 같은 Built-in 객체의 공유된 정의는 Module 내에서 접근 가능하며, 그 객체에 대한 변경은 다른 Module에 반영됩니다.

예를 들어, 모든 ArraypushAll() 메서드를 추가하려면 다음과 같이 Module을 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
// export 또는 import가 없는 module 코드
Array.prototype.pushAll = function(items) {
// items은 array여야만 합니다.
if (!Array.isArray(items)) {
throw new TypeError("Argument must be an array.");
}
// Built-in된 push() 및 spread 연산자 사용
return this.push(...items);
};

이것은 export 또는 import가 없더라도 유효한 Module입니다. 이 코드는 Module과 script로 모두 사용할 수 있습니다. 아무 것도 export할 수 없기 때문에 import를 사용하여 바인딩을 가져 오지 않고 Module 코드를 실행할 수 있습니다.

이 코드는 pushAll() 메서드가 포함된 Moduleimport해서 실행하므로 pushAll()Array 프로토 타입에 추가됩니다. 즉, 이 Module 내부의 모든 Array에서 pushAll()을 사용할 수 있습니다.

바인딩이없는 import하는 대부분의 경우 polyfill과 shim을 만드는 데 사용됩니다.

Module 로딩

ECMAScript 6에서는 Module 구문을 정의하지만 로드하는 방법은 정의하지 않습니다. 모든 JavaScript 환경에서 작동할 수있는 단일 사양을 작성하는 대신 ECMAScript 6은 구문만 지정하고 로드 메커니즘을 HostResolveImportedModule이라는 정의되지 않은 내부 작업으로 추상화합니다. 웹 브라우저와 Node.js는 각각의 환경에 적합한 방식으로 HostResolveImportedModule을 구현하는 방법을 결정해야합니다.

웹 브라우저에서 Module 사용하기

ECMAScript 6 이전에도 웹 브라우저에는 웹 응용 프로그램에 JavaScript를 포함시키는 여러 가지 방법이 있었습니다. 이러한 script 로드 옵션은 다음과 같습니다.

  1. 코드를 로드 할 위치를 지정하는 src Attribute와 함께 <script> 요소를 사용하여 JavaScript 코드 파일로드.
  2. src Attribute 없이 <script> 요소를 사용하여 JavaScript 코드를 인라인으로 포함.
  3. 웹 Worker 또는 서비스 Worker와 같이 Worker로 실행되는 JavaScript 코드 파일로드.

Module을 완벽하게 지원하려면 웹 브라우저가 이러한 각 메커니즘을 업데이트 해야했습니다. 이러한 세부 사항은 HTML 사양에 정의되어 있으며 이 섹션에서 설명합니다.

공유하기