ES2015(ES6)의 Tip & Tricks, 좋은 활용사례들과 코드 예제들이 포함된 cheatsheet입니다. 이 문서는 한국어 번역 버전입니다. 오역 제보와 더 좋은 번역을 기다리고 있습니다!
- var VS let / const
- IIFE를 블록으로 교체하기
- 애로우 펑션
- 문자열
- Destructuring
- 모듈
- 파라미터(Parameter)
- 클래스
- 심볼(Symbol)
- 맵(Map)
- 위크맵(WeakMap)
- Promise
- 제너레이터(Generator)
- Async Await
- Getter/Setter 함수
기존의
var
에 더해let
과const
라는 값을 저장하기 위한 두 개의 새로운 식별자가 추가되었습니다.var
와는 다르게,let
과const
상태는 스코프 내 최상단으로 호이스팅되지 않습니다.
다음은 var
를 활용한 예제입니다.
var snack = '허니버터칩';
function getFood(food) {
if (food) {
var snack = '스윙칩';
return snack;
}
return snack;
}
getFood(false); // undefined
그러나, var
대신 let
을 사용하면 다음과 같이 동작합니다.
let snack = '허니버터칩';
function getFood(food) {
if (food) {
let snack = '스윙칩';
return snack;
}
return snack;
}
getFood(false); // '허니버터칩'
이런 변경점으로 인해 var
를 사용했던 레거시 코드를 리팩토링할 때 더욱 조심해야 합니다. 무턱대고 var
대신 let
을 사용하면 예상치 못한 동작을 할 수도 있습니다.
Note:
let
과const
는 블록 스코프 식별자입니다. 따라서, 블록 스코프 식별자로 정의하기 전에 참조하게 되면ReferenceError
를 발생시킵니다.
console.log(x);
let x = 'hi'; // ReferenceError: x is not defined
Best Practice: 더 조심스럽게 리팩토링하기 위해서 레거시 코드 내에
var
선언을 남겨두세요. 새로운 코드베이스에서 작업하게 될 때, 변수 사용을 위해서let
을 사용하고, 상수 사용을 위해서const
를 사용하세요.
**Immediately Invoked Function Expressions(IIFE)**는 일반적으로 변수들을 별도의 스코프 안에서만 쓰기 위해서 사용되었습니다. ES6에서는 블록을 기반으로 스코프를 만들 수 있게 되었으므로, 더 이상 함수 기반으로 스코프를 만들지 않아도 됩니다.
(function () {
var food = '허니버터칩';
}());
console.log(food); // Reference Error
ES6 블록을 사용하는 경우,
{
let food = '허니버터칩';
};
console.log(food); // Reference Error
종종 다음과 같이 중첩된 함수 안에서 this
의 문맥(context)를 보존해야 할 일이 있습니다.
function Person(name) {
this.name = name;
}
Person.prototype.prefixName = function (arr) {
return arr.map(function (character) {
return this.name + character; // Cannot read property 'name' of undefined
});
};
보통 이 문제를 해결하기 위해 다음과 같이 별도의 변수를 사용해서 this
의 문맥을 저장합니다.
function Person(name) {
this.name = name;
}
Person.prototype.prefixName = function (arr) {
var that = this; // this의 문맥을 저장합니다.
return arr.map(function (character) {
return that.name + character;
});
};
또는, 다음과 같은 방법으로 this
문맥을 통과시킬 수도 있습니다.
function Person(name) {
this.name = name;
}
Person.prototype.prefixName = function (arr) {
return arr.map(function (character) {
return this.name + character;
}, this);
};
이 뿐만 아니라 문맥을 bind 할 수도 있습니다.
function Person(name) {
this.name = name;
}
Person.prototype.prefixName = function (arr) {
return arr.map(function (character) {
return this.name + character;
}.bind(this));
};
애로우 펑션을 사용하면, this
의 문맥 값이 사라지지 않기 때문에 위의 코드는 다음과 같이 다시 쓸 수 있습니다.
function Person(name) {
this.name = name;
}
Person.prototype.prefixName = function (arr) {
return arr.map(character => this.name + character);
};
Best Practice:
this
의 문맥 값을 보존해야할 때마다 애로우 펑션을 사용하세요.
또한 애로우 펑션은 간단한 값을 리턴하는 함수(함수 표현식)가 필요할 때 사용하면 더욱 간결합니다.
var squares = arr.map(function (x) { return x * x }); // 함수 표현식
const arr = [1, 2, 3, 4, 5];
const squares = arr.map(x => x * x); // 간결한 구현을 위한 애로우 펑션
Best Practice: 가능하다면 함수 표현식 대신 애로우 펑션을 활용하세요.
ES6에서는 표준 라이브러리가 크게 확장되었습니다. 이러한 변경에 맞춰 문자열에도 .includes()
와 .repeat()
같은 새로운 메소드가 추가되었습니다.
var string = 'food';
var substring = 'foo';
console.log(string.indexOf(substring) > -1);
문자열 포함 여부를 구현하기 위해서 리턴 값이 -1
보다 큰 지 체크하는 것 대신, 간단하게 불린 값을 리턴하는 .includes()
메소드를 사용할 수 있습니다.
const string = 'food';
const substring = 'foo';
console.log(string.includes(substring)); // true
function repeat(string, count) {
var strings = [];
while(strings.length < count) {
strings.push(string);
}
return strings.join('');
}
ES6에서는 이제 간결하게 구현할 수 있습니다.
// String.repeat(numberOfRepetitions)
'야옹'.repeat(3); // '야옹야옹야옹'
템플릿 리터럴을 사용하면 명시적인 문자열 이스케이프를 사용하지 않아도 특수문자를 포함한 문자열을 구축할 수 있습니다.
var text = "이 문자열은 이스케이프 된 \"큰 따옴표\"를 포함합니다.";
let text = `이 문자열은 이스케이프 된 "큰 따옴표"를 포함합니다.`;
템플릿 리터럴은 문자열과 값을 연결시키는 문자열 Interpolation도 지원합니다.
var name = '나비';
var age = 13;
console.log('제 고양이의 이름은 ' + name + '이고, 나이는 ' + age + '살 입니다.');
더 간단하게 구현하면,
const name = '나비';
const age = 13;
console.log(`제 고양이의 이름은 ${name}이고, 나이는 ${age}살 입니다.`);
ES5에서는 개행을 구현하기 위해서 다음과 같이 했습니다.
var text = (
'고양이\n' +
'강아지\n' +
'투니버스'
);
혹은 이렇게,
var text = [
'고양이',
'강아지',
'투니버스'
].join('\n');
템플릿 리터럴은 명시적으로 표시하지 않아도 개행을 보존합니다.
let text = ( `고양이
강아지
투니버스`
);
뿐만 아니라, 템플릿 리터럴은 표현식에도 접근할 수 있습니다.
let today = new Date();
let text = `현재 시각은 ${today.toLocaleString()}입니다.`;
Destructuring은 배열 혹은 객체(깊게 중첩된 것도 포함하여)에서 편리한 문법을 이용해 값을 추출하고 저장하는데에 활용됩니다.
var arr = [1, 2, 3, 4];
var a = arr[0];
var b = arr[1];
var c = arr[2];
var d = arr[3];
let [a, b, c, d] = [1, 2, 3, 4];
console.log(a); // 1
console.log(b); // 2
var luke = { occupation: 'jedi', father: 'anakin' };
var occupation = luke.occupation; // 'jedi'
var father = luke.father; // 'anakin'
let luke = { occupation: 'jedi', father: 'anakin' };
let {occupation, father} = luke;
console.log(occupation); // 'jedi'
console.log(father); // 'anakin'
ES6 이전엔, 클라이언트 단은 Browserify, Node.js에서는 require같은 라이브러리를 사용했습니다. 이제 ES6에서는 모든 종류(AMD와 CommonJS)의 모듈을 직접적으로 사용할 수 있습니다.
module.exports = 1;
module.exports = { foo: 'bar' };
module.exports = ['foo', 'bar'];
module.exports = function bar () {};
ES6에서는, 다양한 방식으로 모듈을 내보낼 수 있습니다. 그 중 **지정 내보내기(named export)**방식은 다음과 같습니다.
export let name = 'David';
export let age = 25;
또한 객체를 이용한 **리스트 내보내기(exporting a list)**방식도 있습니다.
function sumTwo(a, b) {
return a + b;
}
function sumThree(a, b, c) {
return a + b + c;
}
export { sumTwo, sumThree };
간단하게 export
키워드만 활용하면 함수, 객체, 값 등을 내보낼 수 있습니다.
export function sumTwo(a, b) {
return a + b;
}
export function sumThree(a, b, c) {
return a + b + c;
}
마지막으로, **디폴트 모듈로 내보내기(default binding export)**도 가능합니다.
function sumTwo(a, b) {
return a + b;
}
function sumThree(a, b, c) {
return a + b + c;
}
let api = {
sumTwo,
sumThree
};
export default api;
/* 위 코드는 아래와 같습니다.
* export { api as default };
*/
Best Practice:
export default
메소드는 항상 모듈 코드의 마지막에 위치해야 합니다. 그래야 내보내는 것이 무엇인지 분명해지며, 내보내는 값의 이름을 확인하는 시간을 절약할 수 있습니다. 그 이상으로, CommonJS의 일반적인 관행은 단일 값이나 객체를 내보내는 것입니다. 이런 컨벤션을 따름으로서, 코드의 가독성을 좋게 만들 수 있고 CommonJS와 ES6 모듈을 모두 사용할 수 있게 됩니다.
ES6에서는 다양한 방식으로 모듈을 불러올 수 있습니다. 다음과 같이 파일 전체를 불러올 수 있습니다.
import 'underscore';
이렇게 단순히 파일 전체를 불러오면 그 파일의 최상단에서 불러온 모든 코드가 실행된다는 점에 유의하시기 바랍니다.
파이썬하고 유사한 지정 불러오기(named import)를 사용할 수 있습니다.
import { sumTwo, sumThree } from 'math/addition';
다음과 같이 불러온 모듈의 이름을 새로 작성할 수도 있습니다.
import {
sumTwo as addTwoNumbers,
sumThree as sumThreeNumbers
} from 'math/addition';
거기에 더해서, 모두 불러오기(네임스페이스 불러오기)도 가능합니다.
import * as util from 'math/addition';
마지막으로, 모듈에서 값들의 리스트를 불러올 수도 있습니다.
import * as additionUtil from 'math/addition';
const { sumTwo, sumThree } = additionUtil;
디폴트 모듈은 다음과 같이 불러올 수 있습니다.
import api from 'math/addition';
// 위 코드는 이렇게 표현할 수도 있습니다: import { default as api } from 'math/addition';
가급적 간단한 형태로 모듈을 내보내는 것이 좋지만, 필요하다면 때때로 디폴트 모듈을 포함해 여러 이름을 섞어서 내보낼 수도 있습니다.
// foos.js
export { foo as default, foo1, foo2 };
이 모듈은 아래와 같이 불러올 수 있습니다.
import foo, { foo1, foo2 } from 'foos';
React처럼 CommonJS 문법을 사용해 내보낸 모듈을 불러올 때는 다음과 같이 쓰면 됩니다.
import React from 'react';
const { Component, PropTypes } = React;
다음과 같이 더욱 간결해질 수도 있습니다.
import React, { Component, PropTypes } from 'react';
Note: 내보내지는 값은 참조되는 것이 아니라 바인딩되는 것입니다. 그러므로, 어떤 모듈의 변수 바인딩을 바꾸게 되면 내보낸 모듈 내에서만 바뀌게 됩니다. 이렇게 내보낸 모듈의 값의 인터페이스를 바꾸는 것은 피하세요.
ES5에서는 **디폴트 값(default values)**이나 정의되지 않은 인자(indefinite arguments) 혹은 **네임드 파라미터(named parameters)**를 다루는 함수를 구현하는 방법이 너무 많았습니다. ES6에서는 더욱 간결한 문법을 통해 이것들을 모두 다룰 수 있습니다.
function addTwoNumbers(x, y) {
x = x || 0;
y = y || 0;
return x + y;
}
ES6에서는 함수 내 파라미터의 디폴트 값을 간단하게 설정할 수 있습니다.
function addTwoNumbers(x=0, y=0) {
return x + y;
}
addTwoNumbers(2, 4); // 6
addTwoNumbers(2); // 2
addTwoNumbers(); // 0
ES5에서는 인수의 숫자가 가변적인 경우 다음과 같이 처리했습니다.
function logArguments() {
for (var i=0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
**레스트(rest)**연산자를 사용하면, 다음과 같이 가변적인 숫자의 인수를 넘길 수 있습니다.
function logArguments(...args) {
for (let arg of args) {
console.log(arg);
}
}
ES5의 네임드 파라미터를 처리하는 방법 중 하나는 jQuery에서 차용된 options object 패턴을 사용하는 것입니다.
function initializeCanvas(options) {
var height = options.height || 600;
var width = options.width || 400;
var lineStroke = options.lineStroke || 'black';
}
파라미터에 destructuring을 사용하면 같은 기능을 구현할 수 있습니다.
function initializeCanvas(
{ height=600, width=400, lineStroke='black'}) {
// 여기에서 height, width, lineStroke 변수를 사용합니다.
}
만약 모든 파라미터를 선택적으로 넘기고 싶다면, 다음과 같이 빈 객체로 destructuring 하면 됩니다.
function initializeCanvas(
{ height=600, width=400, lineStroke='black'} = {}) {
// ...
}
ES5에서는 배열 내 숫자들의 최대 값을 찾기 위해서 Math.max
에 apply
메소드를 사용했습니다.
Math.max.apply(null, [-1, 100, 9001, -32]); // 9001
ES6에서는 이제 전개 연산자를 이용해서 함수에 파라미터로 배열을 넘길 수 있습니다.
Math.max(...[-1, 100, 9001, -32]); // 9001
다음과 같이 직관적인 문법을 통해 쉽게 배열 리터럴을 합칠 수도 있습니다.
let cities = ['서울', '부산'];
let places = ['여수', ...cities, '제주']; // ['여수', '서울', '부산', '제주']
ES6 이전에는, 생성자 함수(constructor)를 만들고 프로토타입을 확장해서 프로퍼티를 추가하여 클래스를 구현했습니다.
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
Person.prototype.incrementAge = function () {
return this.age += 1;
};
그리고 다음과 같이 클래스를 상속했습니다.
function Personal(name, age, gender, occupation, hobby) {
Person.call(this, name, age, gender);
this.occupation = occupation;
this.hobby = hobby;
}
Personal.prototype = Object.create(Person.prototype);
Personal.prototype.constructor = Personal;
Personal.prototype.incrementAge = function () {
Person.prototype.incrementAge.call(this);
this.age += 20;
console.log(this.age);
};
ES6는 이런 간단한 구현을 위해 편리한 문법을 제공합니다. 이제 클래스를 직접 만들 수 있습니다.
class Person {
constructor(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
incrementAge() {
this.age += 1;
}
}
그리고 extends
키워드를 사용해서 상속할 수 있습니다.
class Personal extends Person {
constructor(name, age, gender, occupation, hobby) {
super(name, age, gender);
this.occupation = occupation;
this.hobby = hobby;
}
incrementAge() {
super.incrementAge();
this.age += 20;
console.log(this.age);
}
}
Best Practice: 클래스를 만들기 위한 ES6의 문법은 내부적으로 어떻게 프로토타입으로 구현되는지 모호하지만, 초보자들에게 좋은 기능이고 더 깨끗한 코드를 작성하도록 도와줍니다.
심볼은 ES6 이전에도 존재했지만, 이제 직접적으로 심볼을 사용할 수 있는 공식 인터페이스가 제공됩니다. 심볼은 고유하고 수정 불가능한 데이터 타입이고 모든 객체의 식별자로 활용할 수 있습니다.
Symbol()
혹은 Symbol(description)
메소드를 호출하면 전역적으로 사용할 수 없는 고유한 심볼이 생성될 것입니다. Symbol()
은 써드 파티 라이브러리의 객체 혹은 네임스페이스에 충돌할 염려가 없는 새로운 코드를 덧입히는데 종종 쓰입니다. 예를 들어, 나중에 라이브러리가 업데이트 되더라도 겹칠 우려가 없이 React.Component
클래스에 refreshComponent
메소드를 추가하고 싶다면 다음과 같이 할 수 있습니다.
const refreshComponent = Symbol();
React.Component.prototype[refreshComponent] = () => {
// do something
}
Symbol.for(key)
는 여전히 고유하고 수정 불가능한 심볼을 생성하지만, 전역적으로 사용 가능합니다. Symbol.for(key)
를 두 번 호출하면 두 번 다 같은 심볼 인스턴스를 반환합니다. 주의하세요. Symbol(description)
에서는 그렇지 않습니다.
Symbol('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol.for('foo') // true
일반적으로 심볼, 특히 Symbol.for(key)
은 상호 운용성을 위해 사용합니다. 상호 운용성은 몇 가지 알려진 인터페이스를 포함하는 서드 파티 라이브러리의 객체에 인자로 심볼 멤버 형태의 코드를 사용함으로서 만족될 수 있습니다. 예를 들면,
function reader(obj) {
const specialRead = Symbol.for('specialRead');
if (obj[specialRead]) {
const reader = obj[specialRead]();
// do something with reader
} else {
throw new TypeError('객체를 읽을 수 없습니다.');
}
}
또 다른 라이브러리에선 이렇게 할 수 있습니다.
const specialRead = Symbol.for('specialRead');
class SomeReadableType {
[specialRead]() {
const reader = createSomeReaderFrom(this);
return reader;
}
}
상호 운용성을 위해 심볼을 사용하는 주목할 만한 예는 모든 반복 가능한(iterable) 타입 혹은 반복자(iterator)에 존재하는
Symbol.iterator
입니다. 배열, 문자열, 생성자, 등 반복 가능한 타입은 이 메소드를 통해 호출되면 반복자 인터페이스를 포함한 객체 형태로 리턴됩니다.
맵은 자바스크립트에서 자주 필요한 데이터 구조입니다. ES6 이전엔 객체를 이용해서 해시 맵을 생성했습니다.
var map = new Object();
map[key1] = 'value1';
map[key2] = 'value2';
하지만, 이 방법은 특정 프로퍼티 이름으로 인한 예상치 못한 함수 오버라이드(override)로부터 안전하지 않습니다.
> getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned');
> TypeError: Property 'hasOwnProperty' is not a function
실제로 맵은 값을 위해 get
, set
그리고 search
등의 메소드를 제공합니다.
let map = new Map();
> map.set('name', '현섭');
> map.get('name'); // 현섭
> map.has('name'); // true
맵의 가장 놀라운 점은 더 이상 키 값으로 문자열만 쓰지 않아도 된다는 것입니다. 이제 키 값으로 어떤 타입을 전달해도 문자열로 형변환되지 않습니다.
let map = new Map([
['이름', '현섭'],
[true, 'false'],
[1, '하나'],
[{}, '객체'],
[function () {}, '함수']
]);
for (let key of map.keys()) {
console.log(typeof key);
// > string, boolean, number, object, function
}
Note: 함수나 객체처럼 기본형 데이터 타입이 아닌 타입을 사용하면
map.get()
같은 메소드를 사용할 때 비교 연산자가 제대로 동작하지 않습니다. 따라서, 문자열, 불린, 숫자 같은 기본형 데이터 타입을 계속 쓰는 것이 좋습니다.
또한 .entries()
를 사용하면 맵을 순회할 수 있습니다.
for (let [key, value] of map.entries()) {
console.log(key, value);
}
ES6 이전에는 private 데이터를 저장하기 위해서 많은 방법을 사용했습니다. 그 중 한가지가 네이밍 컨벤션을 이용한 방법이죠.
class Person {
constructor(age) {
this._age = age;
}
_incrementAge() {
this._age += 1;
}
}
그러나 네이밍 컨벤션은 코드베이스에 대해 혼란을 일으킬 수 있고, 항상 유지된다는 보장을 할 수 없었습니다. 이제 위크맵으로 이런 값을 저장할 수 있습니다.
let _age = new WeakMap();
class Person {
constructor(age) {
_age.set(this, age);
}
incrementAge() {
let age = _age.get(this) + 1;
_age.set(this, age);
if (age > 50) {
console.log('중년의 위기');
}
}
}
Private 데이터를 저장하기 위해 위크맵을 사용해서 좋은 점은 Reflect.ownKeys()
를 사용해도 프로퍼티 이름을 드러내지 않는다는 것입니다.
> const person = new Person(50);
> person.incrementAge(); // '중년의 위기'
> Reflect.ownKeys(person); // []
위크맵을 사용하는 더욱 실질적인 예는 DOM 요소 자체를 훼손시키지 않고도 DOM 요소에 관련된 데이터를 저장하는 것입니다.
let map = new WeakMap();
let el = document.getElementById('someElement');
// 요소에 대한 약한 참조(weak reference)를 저장
map.set(el, '참조');
// 요소의 값에 접근
let value = map.get(el); // '참조'
// 참조 제거
el.parentNode.removeChild(el);
el = null;
value = map.get(el); // undefined
위에서 보여준 대로, 객체가 가비지 콜렉터에 의해 한 번 제거된 다음에는 위크맵이 자동적으로 해당 객체에 의해 식별되는 key-value 쌍을 제거합니다.
Note: 더 나아가, 이 예제의 유용함을 보여주기 위해 jQuery가 참조를 가진 DOM 요소에 대응되는 객체의 캐시를 저장하는 방법을 생각해보세요. 위크맵을 사용하면, jQuery는 문서에서 지워진 특정 DOM 요소에 관련된 모든 메모리를 자동적으로 절약할 수 있습니다. 전반적으로, 위크맵은 DOM 요소를 감싸는 모든 라이브러리에 매우 유용합니다.
Promise는 다음과 같이 수평적인 코드(콜백 지옥)의 형태를 바꿀 수 있게 해줍니다.
func1(function (value1) {
func2(value1, function (value2) {
func3(value2, function (value3) {
func4(value3, function (value4) {
func5(value4, function (value5) {
// Do something with value 5
});
});
});
});
});
수직적인 코드로 바꾸면,
func1(value1)
.then(func2)
.then(func3)
.then(func4)
.then(func5, value5 => {
// Do something with value 5
});
ES6 이전엔, bluebird 혹은 Q같은 라이브러리를 사용했었습니다. 이제는 Promise가 네이티브로 지원됩니다.
new Promise((resolve, reject) =>
reject(new Error('Promise가 제대로 동작하지 않았습니다!')))
.catch(reason => console.log(reason));
Promise가 제대로 동작(fulfill)했을 때 호출되는 resolve 메소드와 Promise가 제대로 동작하지 않(rejected)았을 때 호출되는 reject 메소드를 이용해 Promise를 다룰 수 있습니다.
Promise의 장점: 중첩된 콜백 코드에서는 에러 핸들링하기가 혼란스럽습니다. Promise를 사용하면 에러를 적절히 위로 깨끗하게 전파할 수 있습니다. 게다가, resolve/reject된 후의 Promise의 값은 불변입니다.
아래는 Promise를 사용하는 실질적인 예제입니다.
var request = require('request');
return new Promise((resolve, reject) => {
request.get(url, (error, response, body) => {
if (body) {
resolve(JSON.parse(body));
} else {
resolve({});
}
});
});
또한 Promise.all()
을 사용해서 비동기 동작들의 배열을 다루는 Promise를 병렬화할 수 있습니다.
let urls = [
'/api/commits',
'/api/issues/opened',
'/api/issues/assigned',
'/api/issues/completed',
'/api/issues/comments',
'/api/pullrequests'
];
let promises = urls.map((url) => {
return new Promise((resolve, reject) => {
$.ajax({ url: url })
.done((data) => {
resolve(data);
});
});
});
Promise.all(promises)
.then((results) => {
// Do something with results of all our promises
});
Promise를 이용해 콜백 지옥을 피하는 것과 비슷하게, 제너레이터도 비동기적인 동작을 동기적인 느낌으로 만들어서 코드를 평평(flat)하게 만들 수 있도록 해줍니다. 제너레이터는 근본적으로 코드의 실행을 중지하고 나중에 표현식의 값을 돌려주는 함수입니다.
다음은 제너레이터를 사용하는 간단한 예입니다.
function* sillyGenerator() {
yield 1;
yield 2;
yield 3;
yield 4;
}
var generator = sillyGenerator();
> console.log(generator.next()); // { value: 1, done: false }
> console.log(generator.next()); // { value: 2, done: false }
> console.log(generator.next()); // { value: 3, done: false }
> console.log(generator.next()); // { value: 4, done: false }
next를 사용하면 제너레이터를 전진시키고 새로운 표현식을 계산합니다. 위의 예제는 극히 부자연스럽지만, 다음과 같이 제너레이터는 비동기적인 코드를 동기적인 방식으로 작성하는데에 활용할 수 있습니다.
// 제너레이터를 이용해 비동기 동작을 숨김
function request(url) {
getJSON(url, function(response) {
generator.next(response);
});
}
그리고 데이터를 돌려줄 제너레이터 함수를 작성합니다.
function* getData() {
var entry1 = yield request('http://some_api/item1');
var data1 = JSON.parse(entry1);
var entry2 = yield request('http://some_api/item2');
var data2 = JSON.parse(entry2);
}
yield
덕분에, data1
에 데이터가 파싱될 필요가 있을 때에만 entry1
이 데이터를 가질 것임을 보장할 수 있습니다.
제너레이터는 비동기적인 코드를 동기적인 방식으로 작성하는데 도움을 주지만, 에러 전파는 깨끗하지 않고 쉽지 않은 경로를 통해 해야 합니다. 그렇기 때문에, Promise를 통해 제너레이터를 보완할 수 있습니다.
function request(url) {
return new Promise((resolve, reject) => {
getJSON(url, resolve);
});
}
그리고 next
를 사용하는 제너레이터를 통해 단계별로 진행하는 함수를 작성합니다. next
는 request
메소드를 사용하여 위의 Promise를 돌려줍니다.
function iterateGenerator(gen) {
var generator = gen();
(function iterate(val) {
var ret = generator.next();
if(!ret.done) {
ret.value.then(iterate);
}
})();
}
Promise를 통해 제너레이터를 보완함으로서, Promise의 .catch
와 reject
를 활용해 에러 전파를 깨끗하게 할 수 있게 되었습니다. 다음과 같이 새롭게 개선된 제너레이터를 사용하면 전보다 더 간단합니다.
iterateGenerator(function* getData() {
var entry1 = yield request('http://some_api/item1');
var data1 = JSON.parse(entry1);
var entry2 = yield request('http://some_api/item2');
var data2 = JSON.parse(entry2);
});
구현했던 코드는 전처럼 제너레이터를 사용하기 위해서 재활용할 수 있습니다. 제너레이터와 Promise가 비동기적인 코드를 동기적인 방식으로 작성하면서도 에러 전파를 좋은 방법으로 하도록 유지시킬 수 있도록 도와줬지만, 사실, async-await는 같은 이점을 더 간단한 형태로 활용할 수 있도록 제공합니다.
사실 async await
는 곧 나올 ES2016의 기능이지만, 제너레이터와 Promise를 같이써야 할 수 있었던 것들을 더 적은 노력으로 가능하게 합니다.
var request = require('request');
function getJSON(url) {
return new Promise(function(resolve, reject) {
request(url, function(error, response, body) {
resolve(body);
});
});
}
async function main() {
var data = await getJSON();
console.log(data); // undefined 값이 아님!
}
main();
간단한 구현이지만, 제너레이터와 비슷하게 동작하는 코드입니다. 저는 제너레이터 + Promise보다는 async-await
를 사용하는 것을 강력하게 추천드립니다. 여기에서 ES7과 Babel을 통해 실행할 수 있는 좋은 예제를 얻을 수 있습니다.
ES6는 getter와 setter 함수를 지원하기 시작했습니다. 다음 예제를 보세요.
class Employee {
constructor(name) {
this._name = name;
}
get name() {
if(this._name) {
return this._name.toUpperCase() + ' 양';
} else {
return undefined;
}
}
set name(newName) {
if (newName == this._name) {
console.log('이미 같은 이름을 쓰고 있습니다.');
} else if (newName) {
this._name = newName;
} else {
return false;
}
}
}
var emp = new Employee("솔지");
// 내부적으로 get 메소드를 활용
if (emp.name) {
console.log(emp.name); // 솔지 양
}
// 내부적으로 setter를 활용
emp.name = "EXID 솔지";
console.log(emp.name); // EXID 솔지 양
가장 최근의 브라우저들은 객체의 getter와 setter 함수를 지원합니다. set/get 전에 리스너와 전처리작업을 추가하여 계산된 프로퍼티를 위해 getter와 settter를 활용할 수 있습니다.
var person = {
firstName: '솔지',
lastName: '허',
get fullName() {
console.log('이름 Get');
return this.lastName + ' ' + this.firstName;
},
set fullName (name) {
console.log('이름 Set');
var words = name.toString().split(' ');
this.lastName = words[0] || '';
this.firstName = words[1] || '';
}
}
person.fullName; // 허 솔지
person.fullName = 'EXID 솔지';
person.fullName; // EXID 솔지