ES6 In Depth 는 ECMAScript 표준의 6번째 버전(이하 ES6) 에서 JavaScript 프로그래밍언어에 추가된 새로운 기능들에 대한 연속기획물 입니다.

2007년 제가 Mozilla의 자바스크립트 팀에서 시작했을때, 일반적인 자바스크립트 프로그램의 길이는 한줄이라는 유머가 있었습니다.

구글 맵이 오픈한지 2년 뒤였습니다. 그 전까지 자바스크립트의 주된 용도는 폼 검증이었고, 보통 <input onchange=>를 처리하기 위한 코드는 한줄이면 충분했었습니다.

모든 것이 변했습니다. 자바스크립트 프로젝트는 굉장한 크기로 성장했고, 커뮤니티는 규모있는 작업을 위한 도구들을 개발해왔습니다. 당신의 코드를 여러 파일과 디렉토리에 나눠서 하게 해주는 (하지만 여전히 필요에 따라 특정 코드에 접근할 수 있고 , 또한 효율적으로 코드를 로딩할 수 있게 하는) 모듈 시스템은 많은 도구들 중 꼭 필요한 것입니다. 그래서 당연히 자바스크립트는 모듈 시스템을 가지게 되었습니다. 또한 모든 소프트웨어와 높은 수준의 의존성을 설치하기 위한 도구인 패키지 매니저도 있습니다. 당신은 어쩌면 ES6 가 새로운 모듈 문법과 함께하기에는 너무 늦었다고 생각할 지도 모릅니다.

좋습니다, 오늘 우리는 ES6가 이미 존재하는 시스템에 어떤 것을 추가했는지 알게 될 것이고, 앞으로의 표준과 도구들이 그 위에서 잘 만들어질 수 있을지 살펴볼 것입니다. 그러나 우선, ES6 모듈이 어떤 모습인지 살펴보도록 하죠.

Module basics

ES6 모듈은 JS 코드를 포함하는 파일입니다. 특별한 module 키워드는 없고, script 처럼 읽습니다. 두가지 차이가 있습니다.

  • ES6 모듈은 “use strict”; 를 적어넣지 않았더라도 자동적으로 strict-mode 입니다.
  • 모듈안에 import 와 export 를 사용할 수 있습니다.

export 부터 이야기해보도록 하죠. 모듈안에 선언된 모든것은 기본적으로 모듈안에서 지역범위(local) 입니다. 만약 모듈에 선언된 것이 다른 모듈에서 사용되도록 공개되기(public)를 원한다면, 그 기능을 export 해야합니다. 이렇게 하기위해서는 몇가지 방법이 있습니다. 가장 간단한 방법은 export 키워드를 추가하는 것입니다.

// kittydar.js - 이미지 내의 모든 고양이의 위치를 찾음
// (Heather Arthur wrote this library for real)
// (but she didn't use modules, because it was 2013)
export function detectCats(canvas, options) {
var kittydar = new Kittydar(options);
return kittydar.detectCats(canvas);
}
export class Kittydar {
... 이미지 프로세싱을 수행하는 몇가지 메소드 ...
}
// 이 helper function 은 export 되지 않음
function resizeCanvas() {
...
}
...

당신은 top-level 의 function, class, var, let, const 를 export 할 수 있습니다.

그리고 그것이 모듈을 작성하기위해 알아야 할 모든 것입니다! IIFE(즉시실행함수)나 콜백안에 모든것을 넣을 필요가 없습니다. 단지 위에서 처럼 당신이 필요한 모든 것을 선언하세요. 코드는 스크립트가 아닌 모듈이기 때문에, 모든 선언은 모듈내로 범위가 한정될 것이고, 모듈들과 모든 스크립트에 전역범위로 노출되지 않습니다. 모듈의 공개 API를 만들기 위해서는 선언을 export 하면 끝입니다.

export 이외에 모듈안의 코드는 지극히 평범합니다. 객체와 배열같은 전역객체를 사용할 수도 있습니다. 웹 브라우저에서 실행된다면, document와 XMLHttpRequest를 사용할 수 있습니다.

별도의 파일에서, 우리는 detectCats() 함수를 import해서 사용할 수 있습니다.

// demo.js - Kittydar demo program
import {detectCats} from "kittydar.js";
function go() {
var canvas = document.getElementById("catpix");
var cats = detectCats(canvas);
drawRectangles(canvas, cats);
}

모듈로부터 여러개의 이름을 import 해서 다음처럼 작성할 수도 있습니다.

import {detectCats, Kittydar} from "kittydar.js";

import 선언을 포함한 모듈을 실행할때는 import 한 모듈을 먼저 로딩하고, 의존성을 추적해서 깊은 순서대로 각 모듈 내부를 실행하며, 이미 실행된 것은 건너뛰어 순환(cycle)을 피합니다.

그리고 여기까지 살펴본 것들이 모듈의 기본입니다. 매우 간단합니다. 😉

Export lists

export 할 기능에 하나하나 export 키워드를 추가할 필요없이, 중괄호로 목록을 감싸서 한번에 export 할 수 있습니다.

export {detectCats, Kittydar};
// 여기서는 `export` 키워드가 필요하지 않습니다.
function detectCats(canvas, options) { ... }
class Kittydar { ... }

export 목록은 꼭 파일의 처음에 있을 필요는 없습니다. 그것은 모듈 파일의 가장 바깥 범위(top-level scope) 어디에서나 나타낼 수 있습니다. 또한 export된 이름이 중복만 아니라면, 여러개의 export 목록이나 다른 export 선언과 함께 섞은 export 목록을 가질 수 있습니다.

Renaming imports and exports

종종 import한 이름이 다른 이름과 충돌이 일어나는 경우가 있습니다. 그래서 ES6는 import 할때 이름을 재정의할 수 있도록 했습니다.

// suburbia.js
// Both these modules export something named `flip`.
// To import them both, we must rename at least one.
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";
...

마찬가지로 export 할때도 이름을 재정의할 수 있습니다. 이것은 종종 같은 값을 두개의 다른 이름으로 export 할때 편리합니다.

// unlicensed_nuclear_accelerator.js - media streaming without drm
// (not a real library, but maybe it should be)
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};

Default exports

새로운 표준은 기존에 많이 사용되고 있는 CommonJS와 AMD 모듈과 호환되도록 설계되었습니다. Node 프로젝트를 하고있고 npm install lodash를 했다고 가정해 봅시다. 당신의 ES6 코드는 Lodash로부터 각각의 함수를 import 할 수 있습니다.

import {each, map} from "lodash";
each([3, 2, 1], x => console.log(x));

그러나 아마 each보다는 _.eash가 익숙할 것이고 그렇게 사용하길 원할 것입니다. 또는 Lodash 에서처럼 함수로서 _를 사용하길 원할 것입니다.

그렇게 하기 위해 약간 다른 문법을 사용할 수 있습니다. 바로 중괄호 없이 모듈을 import 하는 것입니다.

import _ from "lodash";

이 단축문법은 import {default as _} from “lodash”; 와 동일합니다. 모든 CommonJS와 AMD 모듈은 default export를 통해 ES6에 존재하고, 모듈을 위해 require()를 요청하는 것과 같은 것을 얻을 수 있습니다 — 즉, exports 객체입니다.

ES6 모듈은 여러개를 export 하도록 설계되었지만, CommonJS 모듈을 위해서는 default export만 사용할 수 있습니다. 예를 들어 이렇게 작성하면, 유명한 colors 패키지는 제가 보기에는 ES6의 어떤 특별한 지원도 받지 못합니다. 그것은 npm 에 있는 대부분의 패키지들처럼 CommonJS 모듈의 컬렉션 하나입니다. 그러나 당신은 ES6코드에서 그것을 올바르게 import 할 수 있습니다.

// ES6 equivalent of `var colors = require("colors/safe");`
import colors from "colors/safe";

만약 당신이 소유한 ES6모듈을 default export 하길 원한다면 그것은 매우 쉽습니다. default export에는 어떤 마법도 없고, “default” 키워드가 있는 것을 제외하고는 다른 export 와 같습니다. 앞에서 이야기 했듯이 이름을 재정의하는 문법도 사용할 수 있습니다.

let myObject = {
field1: value1,
field2: value2
};
export {myObject as default};

또는 더 개선해서 단축 문법을 사용할 수도 있습니다.

export default {
field1: value1,
field2: value2
};

export default 키워드는 function과 class, object literal, 당신이 이름지은 어떤 값에도 사용할 수 있습니다.

Module objects

너무 길어졌군요. 그러나 몇가지 이유로 모든 언어에서 모듈 시스템은 개별적으로 각자의 특징과 따분한 컨벤션을 가지는 경향이 있고 자바스크립트도 예외가 아닙니다. 운좋게도 한가지만 있습니다. 아 두가지군요.

import * as cows from "cows";

import *를 사용하면, module namespace object 가 import 됩니다. 그 객체의 properties는 module이 export한 것입니다. 그래서 “cows” 모듈이 moo()라는 함수를 export하면, 이 예제처럼 “cows”를 import 한 후에 cows.moo()라고 사용할 수 있습니다.

Aggregating modules

때때로 패키지의 주요모듈은 다른 모듈을 import 하고 통합하여 export 정도만 하는 경우가 있습니다. 이런 종류의 코드를 단순화하기 위해서, import와 export를 한번에 하는 단축문법이 있습니다.

// world-foods.js - good stuff from all over
// "sri-lanka"를 import하고 그 일부를 re-export합니다
export {Tea, Cinnamon} from "sri-lanka";
// "equatorial-guinea"를 import하고 그 일부를 re-export합니다
export {Coffee, Cocoa} from "equatorial-guinea";
// "singapore"를 import하고 그대로 모두 export합니다
export * from "singapore";

이러한 각 export-from문은 import-from문에 export를 사용한 것과 유사합니다. 실제 import 와 달리, 이 문법은 사용한 범위에 re-export된 바인딩을 추가하지 않습니다. 그래서 이 단축문법은 만약 world-foods.js 에서 Tea를 사용하는 코드를 작성할 계획이 있다면 사용하지 말아야 합니다. 그 범위에서는 Tea를 찾을 수 없을 것이기 때문입니다.

만약 “singapore”에 의해 export된 이름이 다른 export된 이름과 충돌하면, 에러가 발생할 것이기 때문에, export *는 주의깊게 써야합니다.

휴! 흥미로운 부분의 문법을 모두 살펴봤습니다!

What does import actually do?

아무것도 믿기지 않는다구요?

신중하시군요. 좋습니다. 당신은 import가 하는 것을 표준이 대부분 말하지 않고 있다고 믿나요? 게다가 좋은 것이긴 할까요?

ES6는 구현에 이르기까지 모듈 로딩의 상세한 부분을 남겼습니다. 모듈 실행의 나머지 부분은 자세하게 서술되어 있습니다.

대략적으로 말하면 당신이 JS 엔진에서 모듈을 실행한다고 말할때, 그것은 다음 4가지 단계를 거쳐 동작합니다.

  1. Parsing: 구현체는 모듈의 소스코드를 읽고 문법 에러를 확인합니다.
  2. Loading: 구현체는 모든 import된 모듈을 (재귀적으로) 로딩합니다. 이것은 아직 표준화되지 않은 부분입니다.
  3. Linking: 각각 새롭게 로딩된 모듈을 위해, 구현체는 모듈 범위(scope)를 만들고 다른 모듈들로부터 import 한 것들을 포함해 모듈에 선언된 모든 바인딩을 채웁니다(fill). 
    만약 import {cake} from “paleo”을 실행했을때, “paleo” 모듈이 cake라는 이름으로 사실상 어떤것도 export 할 수 없다면 에러가 발생할 것입니다. JS code 는 사실상 닫혀있기 때문에 그것은 좋지 않습니다.
  4. Run time: 마지막으로, 구현체는 새롭게 로딩된 모듈의 body에서 코드를 실행합니다. 이때, import 처리는 이미 끝나있으므로 import 선언이 있는 코드의 라인에 도착해 실행했을때… 아무일도 일어나지 않습니다!

알겠나요? 저는 당신에게 “아무것도” 하지 않는다고 대답하겠습니다. 전 프로그래밍 언어에 대해서는 거짓말하지 않습니다.

그러나 지금 이 시스템에서 재미있는 부분을 발견할 수 있습니다. 신기한 트릭인데요. 시스템이 어떻게 로딩하는지를 명시하지 않기 때문에, 그리고 당신이 소스코드에서 import 선언을 미리 찾음으로써 모든 의존성을 알아낼 수 있기 때문에ES6의 구현은 컴파일 타임에 모든 작업이 자유롭고, 당신의 모든 모듈을 하나의 파일로 만들어 네트워크로 보낼 수 있습니다! 그리고 webpack과 같은 툴이 그렇게 하고 있습니다.

이것은 정말 대단합니다, 왜냐하면 네트워크에서 스크립트를 로딩하는 것은 시간이 들고, 당신은 한번에 한개만 가져오며, import 선언에는 수십개 이상을 로딩할 필요가 있다는 것을 알 수 있습니다. native 로더는 많은 network round trip을 필요로 합니다. 그러나 webpack을 사용해서, ES6와 모듈을 사용하는 것 뿐만 아니라 실행시간 성능과 상관없는 소프트웨어 엔지니어링 이점을 얻을 수 있습니다.

ES6에서 모듈로딩에 대한 상세한 명세는 처음부터 계획되어 있었고 만들어졌습니다. 그것이 완성된 표준이 아닌 이유는 어떻게 번들링 기능을 구현할지에 대한 합의가 이루어지지 않았기 때문입니다. 저는 다음에 나열한 이유로 모듈 로딩의 표준화가 어서 해결되기를 바랍니다. 그리고 번들링은 포기하기에는 너무나도 좋은 부분입니다.

Static vs. dynamic, or: rules and how to break them

동적인 프로그래밍 언어에서 자바스크립트는 놀랍게도 정적인 모듈시스템을 가집니다.

  • import와 export는 모듈시스템에서 최고수준(toplevel, 모듈에서 가장 바깥 범위)인 경우에만 허용됩니다. 조건부 import나 export는 없고, 함수 범위내에서 import를 사용할 수 없습니다.
  • export된 모든 식별자는 소스코드 내에서 이름에 의해 명시적으로 export 되어야만 합니다. 당신은 프로그램적으로 배열을 통해 반복할 수 없고 data 지향적인 방법으로 이름을 묶어서 export 해야 합니다.
  • 모듈 객체는 frozen 입니다. 모듈 객체에 polyfill 처럼 새로운 기능을 추가하거나 빼는(hack) 방법은 없습니다.
  • 모듈의 모든 의존성은 모듈 코드가 실행되기 전에 로딩되고 파싱되어 연결(linked)되어야만 합니다. 필요할때 느리게 로딩될 수 있는 import 문법은 없습니다.
  • import 에러를 위한 에러 복구 기능(recovery)은 없습니다. 앱은 아마도 수백개의 모듈을 가지고 있을 것이고, 만약 로딩이나 연결(link)시에 어떤것이라도 실패하면 실행되지 않을 것입니다. try/catch 블록안에서 import 할 수 없습니다. (여기 윗부분은 시스템이 매우 정적이기 때문에, webpack은 그러한 컴파일 타임에 에러를 검사합니다.)
  • 의존성을 로딩하기 전에 모듈이 특정 코드를 실행하도록 허용하는 hook도 없습니다. 이것은 모듈이 그것의 의존성을 로딩하는 방법을 제어할 수 없다는 뜻입니다.

이 시스템은 정적인 시스템으로서는 매우 멋집니다. 그러나 가끔 약간의 hack이 필요할때가 생각날 겁니다. 그렇죠?

그것이 당신이 사용하는 모듈 로딩 시스템이 무엇이든간에 ES6의 정적인import/export 문법과 같은 프로그램적인 API를 가지는 이유입니다. 예를 들어, webpack은 당신이 “code splitting”를 위해 사용할 수 있는 API 를 포함하고, 필요한 타이밍에(lazy) 모듈의 번들을 로딩합니다. 그것과 같은 API로 위에 나열된 다른 대부분의 규칙들을 깰 수 있습니다.

ES6 모듈 문법은 매우 정적이고, 멋집니다 — 그것은 강력한 컴파일 타임 도구의 형태로 성공적입니다. 그 정적인 문법은 굉장히 동적이고 프로그램적인 로더 API와 함께 동작하도록 설계되었습니다.

When can I use ES6 modules?

모듈을 사용하기 위해서, 당신은 TraceurBabel 같은 컴파일러가 필요할 것입니다. 이 시리즈 이전에 Gastón I. Silva s가 Babel과 Broccoli와 함께 보인 부분은 웹에서 ES6를 컴파일하는 방법입니다. 아티클을 작성하면서 Gastón은 ES6 modules 을 지원하는 잘 동작하는 예제를 작성했습니다. Axel Rauschmayer에 의해 작성된 포스트는 Babel과 Webpack을 사용하는 예제를 포함합니다.

ES6 모듈 시스템은 몇년의 논쟁을 통해 저를 포함한 많은 사람들에 반해 시스템의 정적인 부분을 옹호하는 Dave Herman 과 Sam Tobin-Hochstadt에 의해 설계되었습니다. Jon Coppeard는 파이어폭스에 모듈을 구현하고있습니다. JavaScript Loader Standard 의 추가적인 작업은 진행중입니다. HTML에 <script type=module> 와 같은 것을 추가하는 작업은 뒤따를 것으로 기대됩니다.

그리고 그것이 ES6입니다.

끝내고 싶지 않을 만큼 재미있었습니다. 아마 우리는 한개의 에피소드를 더 하게 될 것입니다. 우리는 아티클로서는 큰 메리트가 없는 ES6명세의 나머지부분들에 대해 이야기 할 것입니다. 그리고 아마도 미래에 대해서도 약간 이야기할 것입니다. 다음주 ES6 In Depth의 놀라운 결말에 함께하세요.


발번역은 죄송 ;)

This work is licensed under the Creative Commons 저작자표시-동일조건변경허락 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.