Async 함수 (ES2017)-https://github.com/nhnent/fe.javascript/wiki/%23165:-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D:-Generator-Yield-vs.-Async-Await

자, 제너레이터 함수와 yield가 async/await과 어떤 관련이 있을까? Async/await은 ES2017에서 정식으로 채택된 Javascript에 새롭게 제안된 것이다. 제너레이터보다 더 특별한 방식으로 함수의 실행을 잠시 멈추는 함수를 작성할 수 있다. Async/await을 사용하면 제너레이터의 일부 사용 사례를 더 쉽게 구현할 수 있다는 것을 기억해라. 제너레이터 함수/yield와 Async 함수/await은 모두 "기다리는" 비동기 코드를 작성하는 데 사용된다. 그래서 비동기 함수이지만 동기 함수처럼 보이게 한다; 콜백을 사용하지도 않는다.

제너레이터에서부터 시작해보자: yield는 제너레이터의 실행을 어떤 시점에서 멈출 수 있기 때문에, 비동기 요청이 끝날 때까지 기다린 후 다음 코드가 실행되게 할 수 있다. 다음 예제에서 생각해보자:

우리의 애플리케이션이 백엔드로부터 필요한 정보를 받는 "init" 함수를 갖고 있다고 하자. 예를 들어 사용자 목록을 받아올 때는, XHR 요청을 필요로 하기 때문에 비동기 메서드일 것이다. 프로미스를 사용한다면, 우리는 반드시 프로미스가 끝났을 때 실행될 콜백함수를 정의해야 한다.

img04-async-function-using-promise

제너레이터 함수에서는 비동기 함수인 "getUsersFromDataBase"의 종료를 기다리기 위해 yield를 사용하면 된다. 그리고 users에 반환 값을 넣어주면 된다.

img04-async-function-using-yield

이 방식이 확실히 읽기 쉽지만, 엄청 단순하게 동작하는 것은 아니다. 용어가 암시하듯이, "yield" 키워드는 정말 실행 권한을 제너레이터 함수의 호출자에게 맡긴다. 이것은 프로미스를 양도받는 역할을 하는 외부 함수가 있어야 한다는 것을 의미한다. 프로미스가 끝날 때까지 기다렸다가 제너레이터 함수에 반환 값을 넘겨주어, 함수의 실행이 재개되고 이 값이 users라는 변수에 할당되도록 한다.

위 예제는 우리가 이전에 알지 못했던 제너레이터 함수의 특징을 알게 해준다: 제너레이터 함수는 "멈춤 지점"이라면 외부 함수로 값을 반환할 뿐만 아니라, 외부함수로부터 값을 받을 수 있다. 이는 두 코드 간의 back-and-forth 커뮤니케이션을 허용한다.

이터레이터의 "next" 메서드의 변수로 전달된 값은 제너레이터 함수로 전달된다. 다음 예제는 어떻게 제너레이터 함수를 호출하는 "외부" 코드가 프로미스를 끝내고, 반환된 값을 제너레이터 함수로 보내는 역할을 하는지에 대한 설명이다:

img05-how-exteranl-code-get-generators-return-value

getUsersFromDatabase의 구현은 중요하지 않다. 이 함수는 2초 후에 "Test Users"라는 문자열을 반환하면서 종료되는 프로미스를 반환한다. 어떻게 외부 코드(6-30번째 줄)가 제너레이터의 마지막 값을 얻기 위해 제너레이터를 호출하고(6-7번째 줄), 프로미스가 반환한 값을 처리해서(16번째 줄에서 시작하는 콜백), 제너레이터에 전달하는지(20번째 줄)에 주목하자. 13, 18, 28번째 줄은 각 시점의 변수의 값을 보여주기 위한 곳으로, 그 값은 어두운 파란색 글자로 표시되며, 인라인 실행 도구에 의해 추가됐다.

이 예제는 제너레이터 함수가 하나의 값만 양도(yield)하는 특정한 경우에 대한 것으로 단순한 시나리오이다. 이론적으로, 외부 함수는 제너레이터의 마지막 return 구문에 다다르기 까지 지나치는 모든 프로미스를 반환해야 한다. 서드 파티 라이브러리에서도 같은 관계이다. 외부 함수가 프로미스를 어떻게 처리하고 해결하는지 상관하지 않고, 비동기로 기다리는 코드를 generators/yield 방식으로 작성할 수 있게 한다. 이 라이브러리들은 제너레이터 함수를 argument로 받아서, 제너레이터를 실행하고, yield 된 프로미스를 다루는 기능 제공한다.

이 긴 예제는 Async/await이 엄청 유용한 기능인지 보여주기 위한 코드이다: 비동기 코드를 generator 함수 예제(1-4번째 줄)와 비슷한 데, 심지어는 프로미스를 다루기 위한 외부의 헬퍼 함수가 필요하지도 않는다! async/await을 쓰면, 이렇게만 쓰면 된다:

img05-async-function-using-async-await

제너레이터의 별표(*)는 함수 선언부 앞에 오는 async 키워드로, "yield" 키워드는 "await"으로 대체되었다. Await은 프로미스를 반환하는 구문이기만 하면 그 앞에 놓일 수 있다. 그리고 await 키워드는 그 자신보다 먼저 선언된, async 키워드가 있는 함수 안에서만 사용할 수 있다. 이제 테스트 함수가 실행될 때, 다른 함수의 도움없이 await 키워드에서 멈추고, 프로미스가 끝나길 기다렸다 자동으로 프로미스에서 반환된 값을 users라는 const 변수에 할당할 것이다.

Async/await은 프로미스를 .then() 메서드나 콜백 정의, 그리고 이것들의 중첩현상(악명높은 죽음의 피라미드) 없이 다루는 코드를 작성하게 한다. 좋은 해결방법인 것처럼 보이나 이것을 사용하기 전에 반드시 생각해야 할 몇 가지 중요한 점이 있다. 때때로, 이전의 .then()의 프로미스 방식을 고수하는 것이 더 좋은 방법일 수 있다. 다음에 오는 내용을 꼭 생각해봐야 한다:

1) 비동기 함수는 항상 프로미스를 반환한다: 비동기 함수는 모든 "await" 키워드에서 실행을 잠깐 멈추고 비동기 구문이 종료되길 기다린다. 그래서 await이 붙는 함수 자체가 비동기적이다(이 때문에 비동기 함수 앞에 async 키워드가 붙는 것이다). 이는 async 키워드를 가진 함수는 무엇을 반환하든 간에 항상 리졸브되거나 에러를 던지는 프로미스를 반환한다는 것이다. 이전 예제에서 "test" 함수는 문자열 "Test Users Correctly received"라는 문자열을 반환했다. 그러나 실제로는 이 문자열과 함께 해결되는 프로미스가 반환됐다. 그래서 코드를 설계할 때나 코드의 다른 부분이 주어진 함수와 어떻게 상호작용하도록 고민할 때, 프로미스를 받기 원하는지 아닌 지를 꼭 생각해야 한다.

2) Await은 항상 프로미스를 병렬적이 아닌, 순차적으로 기다린다 await 키워드는 한 번에 여러 개 아닌, 하나의 프로미스만 기다릴 수 있다. 그래서 만약 여러 개의 프로미스를 다루면서, 이 각각이 await 키워드를 통해 기다리길 바란다면, 하나의 동작이 완전히 끝나야 다음 동작으로 넘어갈 수 있다.

동시에 수백 개의 요청을 보내 네트워크에 부하를 주는 것을 방지하고 싶을 때처럼 이 방식이 최선일 때가 있지만, 이 방식은 동시에 여러 개의 프로미스를 처리하는 것보다 훨씬 느리다.

img06-sequencial-async-functions

사용자 목록에 대한 배열인 "users"가 이전에 선언되어 있다고 가정하자. 그리고 "getProfileImage"가 프로필 이미지를 반환하는 프로미스를 반환한다고 가정하자. 이 예제는 각각의 사용자들을 순회하는데, "profileImages" 배열에 프로필 이미지를 넣기 위해 매 차례에서 잠깐 멈춘다. 이것은 현재 이터레이션의 프로미스가 끝났을 때만 다음 이터레이션으로 이동한다.

동시에 여러 프로미스를 처리하는 것에 대한 대안은 await과 프로미스를 같이 쓰는 것이다. 예를 들어, "Promise.all"로 프로미스 그룹을 관리할 수 있는 데, 이는 그룹의 모든 프로미스가 끝날(또는 실패 시 에러를 반환) 때 끝나는 하나의 프로미스를 반환한다. 그러면, 이 하나의 통합된 프로미스에 await을 사용하면 된다.

img07-parallel-async-functions

이 예제는 "map"을 사용해서 users 배열을 순회한다: 각각의 차례에서 "getProfileImage"가 실행되고, 실행되지 않는 프로미스를 반환한다. Map은 각 차례에서 일시정시하지 않고, 모든 프로미스를 가지는 배열을 반환한다는 것에 주목하자. 그러고 나서 우리는 이 모든 프로미스들을 "Promise.all"로 묶고, 이 한 개의 프로미스가 해결되는 것을 기다리기 위해 딱 이 시점에서만 await을 사용한다.

기억하자 - 제너레이터와 비동기 함수는 항상 특별한 타입의 객체를 반환한다.

  • Generator 함수: 값 X를 yield/return하면, 이것은 항상 {value: X, done: Boolean} 형태의 이터레이션 객체를 반환한다.
  • 비동기 함수: 값 X를 반환하면, X를 반환하면서 끝나거나 에러를 던지는 프로미스를 반환한다.

결론

제너레이터는 실행을 잠깐 멈출 수 있는 함수이다. 이터레이터 객체가 다음 값을 요청할 때마다 위임된(yield) 값을 생성한다. 이런 의미에서, 제너레이터는 수동적인 생산자인 반면 이터레이터는 적극적인 소비자이다(왜냐하면 값을 요청하는 것에 대한 주도권을 갖고 있기 때문이다). 이것은 일반적인 옵저버 패턴과 대조적이다. 옵저버 패턴에서는 적극적인 생산자(옵저버블, 주체)가 필요할 때 그 값을 반환하고, 하나 이상의 수동적인 소비자(옵저버)가 있어서 값이 반환되기를 기다리고 있다. 제너레이터 함수는 리스트에서 한 번에 하나의 값만 반환하는 데 사용할 수 있다. 아마도 필요에 따라 무한한 값을 생성하는 데 사용할 수 있다.

제너레이터 함수의 특별한 사용법 중의 하나는 프로미스를 양도하고(yielding) 동기식으로 동작하는 것처럼 보이는("기다리는") 비동기 코드를 작성하는 것이다. 그런데 이 방식은 반환되는 프로미스들을 다룰 다른 함수의 도움이 필요하다. 이런 경우에는 헬퍼 함수가 필요없는 async/await을 사용하는 것이 더 나은 방법이다.

비동기 함수와 await 키워드는 비동기 코드를 "기다리는" 방식으로 작성하기 위한 훌륭한 방법이다. 그러나 한 번에 여러 개의 프로미스를 기다릴 수 없기 때문에, 이런 한계 상황에서는 이전의 프로미스 폴백 방식을 사용하는 것이 낫다.







async-await를 사용한  비동기처리와 generator-yield를 사용한 비동기처리의 차이

Link to section

generator-yield의 경우 generator의 리턴값이 iterator으로, 각 yield에서 작업이 중단되면 .next()를 통해 다음의 작업으로 넘어가야 하는 불편함이 있었다. co 모듈이나 aa모듈은 그러한 불편함을 줄이기 위해 사용된 모듈이다.

그러나 async-await의 경우 await 이후에  promise가 오게 되면 promise의 resolve가 반환될때 까지 기다려주기 때문에 별도의 모듈을 사용할 필요가 없다. 


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
31
32
33
34
35
36
37
38
39
40
const co = require('co');
 
const func1 = function() {
    return  new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log('a');
            resolve();
            //reject('func1 fail');
        }, 2000);
    });
  };
 
const func2 = function() {
    return  new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log('b');
            resolve();
            //reject('func2 fail');
        }, 3000);
    });
};
 
const func3 = function() {
    console.log('c');
};
 
//func1().then(func2).then(func3)
    
co(function*(){
    yield func1();
    yield func2();
    yield func3();
    })
    
 
async function add1(){
    await func1();
    await func2();
    await func3();    
}
cs


'C Lang > JS Technic' 카테고리의 다른 글

Stream API입문  (2) 2018.09.05
파라미터의 유효성체크(validation check of parameter)  (0) 2018.08.31
in 연산자  (0) 2018.08.23
for in vs for of 반복문  (0) 2018.08.23
array.shift()  (0) 2018.08.15

[JS #5] ES6 Map(), Set()

얼마 전부터 회사 업무를 진행할 때 본격적으로, 그리고 의식적으로 ES6 에 도입된 문법을 적용하고 있는데, 그중에서 가장 자주 활용하는 자료구조, Map 과 Set 에 대해 이야기해보려고 합니다. 이 글의 모티브는 상당부분 Mozilla 웹기술블로그에 기반합니다. 사내세미나에서 발표한 내용을 글로 정리했습니다.

Map

  • Map() 은 자바스크립트의 key-value 페어(pair) 로 이루어진 컬렉션
  • key 를 사용해서 value 를 get, set 할 수 있음
  • key 들은 중복될 수 없음: 하나의 key 에는 하나의 value 만
  • key 로 사용할 수 있는 데이터형: string, symbol(ES6), object, function >> number 는 사용할 수 없음에 주의!



// 새로운 map 을 만들고 map 에 key, value 엔트리를 추가
let me = new Map();
me.set('name', 'kevin');
me.set('age', 28);
console.log(me.get('age'); // 28
// 대괄호를 사용해서 map 을 선언하는 방법
const roomTypeMap = new Map(
[
["01", "원룸(오픈형)"],
["02", "원룸(분리형)"],
["03", "원룸(복층형)"],
["04", "투룸"],
["05", "쓰리룸"]
]
);
// 새로운 map 을 만들고 그 데이터를 기존의 [key, value] 페어컬렉션으로 채움
let you = new Map().set('name', 'paul').set('age', 34);
console.log(you.get('name')); // 'paul'
// has(): 주어진 key 가 존재하는지 확인
console.log(me.has('name')); // true
// size: map 에 담겨진 엔트리의 개수를 조회
console.log(you.size); // 2
// delete(): 엔트리를 삭제
me.delete('age');
console.log(me.has('age')); // false
// clear(): 모든 엔트리를 삭제
you.clear();
console.log(you.size); // 0

<참고 1> Map 과 Object 비교

  • Object 의 key 는 string 과 symbol(ES6) 만 가능하지만, map 은 어떤 값도 가능
  • Object 에서는 크기를 추적해서 알 수 있지만, map 은 손쉽게 얻을 수 있음(size)

Map 의 iterable object

  • map.keys(), map.values()
  • map 안의 key 혹은 value 들을 순회할 수 있는 iterable object 를 반환
let me = new Map().set('a', 1).set('b', 2);
console.log([...me.keys()]); // ['a', 'b']
console.log([...me.values()]); // [1, 2]
  • map.entries(), map.next()
  • map 안의 모든 엔트리들을 순회할 수 있는 iterable object 를 반환
let you = new Map().set('Seoul', 28).set('Tokyo', 26);
let iterObj = you.entries();
console.log(iterObj.next()); // {value: ['Seoul', 28], done: false}
console.log(iterObj.next()); // {value: ['Tokyo', 26], done: false}
console.log(iterObj.next()); // {value: undefined, done: true}
  • for-of, map.forEach();
  • forEach 의 경우, 인자 순서가 이상한데(key, value 순서가 반대) Array.prototype.forEach() 구문과 통일성을 유지하기 위함(value, index, array 순서인 것)
let we = new Map().set('car', 30).set('bus', 45);
// for-of 로 map 순회하기
for (let [key, value] of we) {
console.log(key + '^' + value);
}
// 차례대로 'car^30', 'bus^45' 출력
// forEach 로 map 순회하기
we.forEach((value, key, map) => {
console.log(key + '$' + value);
});
// 차례대로 'car$30', 'bus$45' 출력
  • 자바스크립트 배열 메서드에 존재하는 map, filter 메서드는 Map 에 존재하지 않는다. 하지만 아래와 같은 방식으로 우회해서 사용이 가능하다.
let me = new Map().set('a', 1).set('b', 2);
// value 가 1 이상인 엔트리만 filtering 하기
let map1 = new Map(
[...me]
.filter(([k, v]) => v > 1)
);
console.log([...map1.entries()]) // [['b', 2]]
// key 뒤에 'super' 문자열을 붙이고, value 에 1을 더하기
let map2 = new Map(
[...me]
.map(([k, v]) => [k + "super", v + 1])
);
console.log([...map2.entries()]) // [['asuper, 2], [bsuper, 3]]



Set

  • Set() 은 value 들로 이루어진 컬렉션(“집합”이라는 표현이 적절)
  • Array 와는 다르게 Set 은 같은 value 를 2번 포함할 수 없음
  • 따라서 Set 에 이미 존재하는 값을 추가하려고 하면 아무 일도 없음





// 비어있는 새로운 set 을 만듬
let setA = new Set();
// 새로운 set 을 만들고 인자로 전달된 iterable 로 인자를 채움
let setB = new Set().add('a').add('b');
setB.add('c');
console.log(setB.size); // 3
// has(): 주어진 값이 set 안에 존재할 경우, true 를 반환
// indexOf() 보다 빠름. 단, index 가 없음
console.log(setB.has('b')); // true
// set 에서 주어진 값을 제거
setB.delete('b');
console.log(setB.has('b')); // false
// set 안의 모든 데이터를 제거
setB.clear();
console.log(setB.size); // 0
  • <TODO> has() 는 indexOf() 보다 빠르다. 다만, index 이 존재하지 않기때문에 index 로 value 로 접근할 수 없다.

<참고 2> Spread 연산자

  • 이터러블 오브젝트(iterable object)의 엘리먼트를 하나씩 분리하여 전개
// string == iterable object
console.log([...'music']); // ['m', 'u', 's', 'i', 'c']

<참고 3> for 문들

  • for 문
let sampleArr = [1, 2, 3, 4, 5];
for (let i = 0, length = sampleArr.length; i < length; i++) {
console.log(sampleArr[i]);
}
  • forEach: ES5 자바스크립트 배열 메서드
let sampleArr = [1, 2, 3, 4, 5];
sampleArr.forEach(v => console.log(v));
  • for-in: Object 를 순회하기 위한
let sampleObj = {
a: 1,
b: 'hello',
c: [1, 2]
}
for (let key in sampleObj) {
console.log(key);
console.log(sampleObj[key]);
}
  • for-of: 배열의 요소들, 즉 data 를 순회하기 위한(string 도 가능)
let sampleArr = [1, 2, 3, 4, 5];
let (for value of sampleArr) {
console.log(value);
}

  • 일반 객체(Object)는 iterable 하지 않다!
  • for-of 나 …(spread 연산자)를 사용할 수 없다!
  • for-in 으로나 순회할 수 있다.


Set 의 iterable object

  • set.values();
  • 기본적으로 Set 의 prototype 메서드로 keys() 는 존재하지 않고, values() 만 존재하지만, MDN 의 설명에 따르면, map 오브젝트와 동일하게 동작하기 때문에 Set.prototype.keys() 는 Set.prototype.values() 와 같은 결과



let setA = new Set();
setA.add('a');
setA.add('b');
setA.add('a');
console.log([...setA.keys()]); // ['a', 'b']
console.log([...setA.values()]); // ['a', 'b']
  • set.entries();
let setB = new Set();
setB.add('Korea');
setB.add('Japan');
setB.add('China');
let entries = setB.entries();
console.log(entries.next()); 
// {value: ['Korea', 'Korea'], done: false}
console.log(entries.next());
// {value: ['Japan', 'Japan'], done: false}
console.log(entries.next());
// {value: ['China', 'China'], done: false}
console.log(entries.next());
// {value: undefined, done: true}
  • for-of, set.forEach();
let setC = new Set();
setC.add('Korea');
setC.add('Japan');
setC.add('China');
for (let key of setC) {
console.log(key);
}
// 차례대로 'Korea', 'Japan', 'China' 출력
setC.forEach((v, k) => {
console.log(v);
})
// 차례대로 'Korea', 'Japan', 'China' 출력

Set: 집합연산

스위프트 집합연산 — 링크
  • union(합집합), intersection(교집합), difference(차집합)
let setA = new Set([1, 2, 3, 4, 5]);
let setB = new Set([4, 5, 6, 7, 8]);
// 합집합
let unionSet = new Set([...setA, ...setB])
for (let value of unionSet) {
console.log(value);
}
// 차례대로 1, 2, 3, 4, 5, 6, 7, 8 출력
// 교집합
let intersectionSet = new Set(
[...setA].filter(v => setB.has(v))
);
for (let value of intersectionSet) {
console.log(value);
}
// 차례대로 4, 5 출력
// 차집합
let differenceSet = new Set(
[...setA].filter(v => !setB.has(v))
);
for (let value of differenceSet) {
console.log(value);
}
// 차례대로 1, 2, 3 출력
  • symmetricDifference
// Symmetric Difference
var set1 = new Set([1, 2, 3, 4, 5]);
var set2 = new Set([3, 4, 5, 6, 7]);
var symmetricDifferenceSet = new Set(
[...[...set1].filter(x => !set2.has(x)), ...[...set2].filter(x => !set1.has(x))]
)
for (let value of symmetricDifferenceSet) {
console.log(value);
}
// 차례대로 1, 2, 6, 7 출력

지금까지 Map 과 Set 에 대해 자세히 알아보았습니다.

단순히 key 와 value 를 set 하거나 value 를 set 하는 것뿐만 아니라, iterable object 의 특성을 살려서 map 과 set 을 순회하는 것을 알아보았습니다. 더 나아가서는 map 과 set 에서는 지원하지 않는 배열 메서드(Array.prototype) 인 map, filter 를 적용해보고, 집합연산까지도 진행해보았습니다.

억지로 배열의 형태로, 기본 객체형태로 코딩하기 보다는 적재적소의 자료구조의 특성에 맞게 코딩하는 습관을 기르면 좋겠습니다.

다음에는 윗글에서도 잠깐 나왔지만, 왜 배열의 indexOf 메서드보다 Set 의 has 가 더 “빠른지" 알아보겠습니다^^ 읽어주셔서 감사합니다.


in 연산자는 명시된 속성이 명시된 객체에 존재하면 true를 반환합니다.

Link to section구문

속성 in 객체명

Link to section인자

속성
속성의 이름이나 배열의 인덱스를 뜻하는 문자열 또는 수 값입니다.
객체명
객체의 이름입니다.

Link to section설명

 다음 예제들은 in 연산자의 용도를 보여 줍니다.

// 배열
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees         // true를 반환합니다.
3 in trees         // true를 반환합니다.
(1 + 2) in trees   // true를 반환합니다. 연산자 우선 순위에 의하여 이 구문의 괄호는 없어도 됩니다.
6 in trees         // false를 반환합니다.
"bay" in trees     // false를 반환합니다. 당신은 배열의 내용이 아닌, 인덱스 값을 명시하여야 합니다.
"length" in trees  // true를 반환합니다. length는 Array(배열) 객체의 속성입니다.

// 미리 정의된 객체
"PI" in Math       // true를 반환합니다.
"P" + "I" in Math  // true를 반환합니다.

// 사용자가 정의한 객체
var myCar = {company: "Lamborghini", model: "Lamborghini Veneno Roadster", year: 2014};
"company" in myCar // true를 반환합니다.
"model" in myCar   // true를 반환합니다.

 당신은 반드시 in 연산자의 오른쪽에 객체를 명시하여야 합니다. 예컨대 당신은 String 생성자로 만들어진 문자열을 명시할 수 있지만 문자열 리터럴은 명시할 수 없습니다.

var color1 = new String("green");
"length" in color1 // true를 반환합니다. string 객체는 length, anchor, big등의 프로퍼티를 가지고 있기때문에 true

var color2 = "coral";
"length" in color2 // color2는 String 객체가 아니리 원시자료형이기 때문에 오류를 냅니다.

Link to section제거되었거나 정의되지 않은 속성에 대하여 in 연산자 사용하기

 in 연산자는 delete 연산자로 제거된 속성에 대하여 false를 반환합니다.

var myCar = {company: "Lamborghini", model: "Lamborghini Veneno Roadster", year: 2014};
delete myCar.company;
"company" in myCar; // false를 반환합니다.

var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
delete trees[3];
3 in trees; // false를 반환합니다.

 만약 당신이 속성을 undefined로 설정하였는데 그것을 제거하지 않으면, in 연산자는 그 속성에 대하여 true를 반환합니다.

var myCar = {company: "Lamborghini", model: "Lamborghini Veneno Roadster", year: 2014};
myCar.company = undefined;
"company" in myCar; // true를 반환합니다.
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
trees[3] = undefined;
3 in trees; // true를 반환합니다.

Link to section상속된 속성

 in 연산자는 프로토타입 체인에 의하여 접근할 수 있는 속성에 대하여 true를 반환합니다.

"toString" in {}; // true를 반환합니다.

Link to section명세

명세StatusComment
ECMAScript Latest Draft (ECMA-262)
The definition of 'Relational Operators' in that specification.
Draft 
ECMAScript 2015 (6th Edition, ECMA-262)
The definition of 'Relational Operators' in that specification.
Standard 
ECMAScript 5.1 (ECMA-262)
The definition of 'The in Operator' in that specification.
Standard 
ECMAScript 3rd Edition (ECMA-262)
The definition of 'The in Operator' in that specification.
Standard초기의 정의가 담겨 있습니다. JavaScript 1.4에 추가되었습니다.

Link to section브라우저 호환성

We're converting our compatibility data into a machine-readable JSON format. This compatibility table still uses the old format, because we haven't yet converted the data it contains. Find out how you can help!

기능ChromeFirefox (Gecko)Internet ExplorerOperaSafari
지원(Yes)(Yes)(Yes)(Yes)(Yes)

Link to section관련 문서



 for in vs for of 반복문



forEach반복문

forEach반복문은 오직 Array 객체에서만 사용가능한 메서드입니다.(ES6부터는 Map, Set 등에서도 지원됩니다) 배열의 요소들을 반복하여 작업을 수행할 수 있습니다. forEach구문의 인자로 callback 함수를 등록할 수 있고, 배열의 각 요소들이 반복될 때 이 callback 함수가 호출됩니다. callback 함수에서 배열 요소의 인덱스와 값에 접근할 수 있습니다.

var items = ['item1', 'item2', 'item3'];

items.forEach(function(item) {
    console.log(item);
});
// 출력 결과: item, item2, item3




for …in 반복문

for in 반복문은 객체의 속성들을 반복하여 작업을 수행할 수 있습니다. 모든 객체에서 사용이 가능합니다. for in 구문은 객체의 key 값에 접근할 수 있지만, value 값에 접근하는 방법은 제공하지 않습니다. 자바스크립트에서 객체 속성들은 내부적으로 사용하는 숨겨진 속성들을 가지고 있습니다. 그 중 하나가 [[Enumerable]]이며, for in 구문은 이 값이 true로 셋팅되어 속성들만 반복할 수 있습니다. 이러한 속성들을 열거형 속성이라고 부르며, 객체의 모든 내장 메서드를 비롯해 각종 내장 프로퍼티 같은 비열거형 속성은 반복되지 않습니다.

var obj = {
    a: 1, 
    b: 2, 
    c: 3
};

for (var prop in obj) {
    console.log(prop, obj[prop]); // a 1, b 2, c 3
}




for …of 반복문

for of 반복문은 ES6에 추가된 새로운 컬렉션 전용 반복 구문입니다. for of 구문을 사용하기 위해선 컬렉션 객체가 [Symbol.iterator] 속성을 가지고 있어야만 합니다(직접 명시 가능).

따라서, 객체 {}는 [Symbol.iterator] 속성을 가지고 있지않으므로 for of 구문을 사용할 수 없습니다.

var iterable = [10, 20, 30];

for (var value of iterable) {
  console.log(value); // 10, 20, 30
}

for in 반복문과 for of 반복문의 차이점

  • for in 반복문 : 객체의 모든 열거 가능한 속성에 대해 반복
  • for of 반복문 : [Symbol.iterator] 속성을 가지는 컬렉션 전용
Object.prototype.objCustom = function () {};
Array.prototype.arrCustom = function () {};

var iterable = [3, 5, 7];
iterable.foo = "hello";

for (var key in iterable) {
  console.log(key); // 0, 1, 2, "foo", "arrCustom", "objCustom"
}

for (var value of iterable) {
  console.log(value); // 3, 5, 7
}



출처: http://itstory.tk/entry/Javascript-for-in-vs-for-of-반복문 [덕's IT Story]

The iterable protocol

iterable protocol 은 JavaScript 객체들이, 예를 들어 for..of 구조에서 어떠한 value 들이 loop 되는 것과 같은 iteration 동작을 정의하거나 사용자 정의하는 것을 허용합니다. 다른 type 들(Object 와 같은)이 그렇지 않은 반면에, 어떤 built-in type 들은 Array 또는 Map 과 같은 default iteration 동작으로 built-in iterables 입니다.

iterable 하기 위해서 object는 @@iterator 메소드를 구현해야 합니다. 이것은 object (또는 prototype chain 의 오브젝트 중 하나) 가 Symbol.iterator key 의 속성을 가져야 한다는 것을 의미합니다 :

PropertyValue
[Symbol.iterator]object를 반환하는, arguments 없는 functioniterator protocol 을 따른다.

어떠한 객체가 반복(Iterate)되어야 한다면 이 객체의 @@iterator 메소드가 인수없이 호출되고, 반환된 iterator는 반복을 통해서 획득할 값들을 얻을 때 사용됩니다.

The iterator protocol

iterator protocol 은 value( finite 또는 infinite) 들의 sequence 를 만드는 표준 방법을 정의합니다. 

객체가 next() 메소드를 가지고 있고, 아래의 규칙에 따라 구현되었다면 그 객체는 iterator이다:

PropertyValue
next

아래 2개의 속성들을 가진 object 를 반환하는 arguments 없는 함수 :

  • done (boolean)
    • Iterator(반복자)가 마지막 반복 작업을 마쳤을 경우 true. 만약 iterator(반복자)에 return 값이 있다면 value의 값으로 지정된다. 반환 값에 대한 설명은 여기.
    • Iterator(반복자)의 작업이 남아있을 경우 false. Iterator(반복자)에 done 프로퍼티 자체를 특정짓지 않은 것과 동일하다.
  • value - Iterator(반복자)으로부터 반환되는 모든 자바스크립트 값이며 done이 true일 경우 생략될 수 있다.

몇몇 iterator들은 iterable(반복 가능)이다:

var someArray = [1, 5, 7];
var someArrayEntries = someArray.entries();

someArrayEntries.toString();           // "[object Array Iterator]"
someArrayEntries === someArrayEntries[Symbol.iterator]();    // true

 Iteration protocols 사용 예시

String 은 built-in iterable 객체의 한 예시입니다.

var someString = "hi";
typeof someString[Symbol.iterator];          // "function"

String 의 기본 iterator 는 string 의 문자를 하나씩 반환합니다.

var iterator = someString[Symbol.iterator]();
iterator + "";                               // "[object String Iterator]"
 
iterator.next();                             // { value: "h", done: false }
iterator.next();                             // { value: "i", done: false }
iterator.next();                             // { value: undefined, done: true }

spread operator와 같은 특정 내장 구조(built-in constructs)들은 실제로는 동일한 iteration protocol을 사용한다:

[...someString]                              // ["h", "i"]

사용자만의 @@iterator를 특정함으로써 원하는 반복 행위(iteration behavior)를 설정할 수 있다:

var someString = new String("hi");          // need to construct a String object explicitly to avoid auto-boxing

someString[Symbol.iterator] = function() {
  return { // this is the iterator object, returning a single element, the string "bye"
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};

재설정된 @@iterator가 어떻게 내장 구조(built-in constructs)의 반복 행위에 영향을 주는지 참고:

[...someString];                              // ["bye"]
someString + "";                              // "hi"

Iterable 예시

내장 iterables

String, Array, TypedArray, Map and Set 는 모두 내장 iterable이다. 이 객체들의 프로토타입 객체들은 모두 @@iterator 메소드를 가지고 있기 때문이다.

사용자 정의된 iterables

이렇게 고유한 iterables 를 만들 수 있다.

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};
[...myIterable]; // [1, 2, 3]

Iterable을 허용하는 내장 API들

Iterable을 허용하는 많은 내장 API들이 있다. 예를 들어: Map([iterable])WeakMap([iterable])Set([iterable]) and WeakSet([iterable])이 그것이다:

var myObj = {};
new Map([[1,"a"],[2,"b"],[3,"c"]]).get(2);               // "b"
new WeakMap([[{},"a"],[myObj,"b"],[{},"c"]]).get(myObj); // "b"
new Set([1, 2, 3]).has(3);                               // true
new Set("123").has("2");                                 // true
new WeakSet(function*() {
    yield {};
    yield myObj;
    yield {};
}()).has(myObj);                                         // true

뿐만 아니라 Promise.all(iterable)Promise.race(iterable)와 Array.from() 또한 해당된다.

Iterable과 함께 사용되는 문법

for-of loops, spread operatoryield* destructuring assignment는 iterable과 함께 사용되는 구문(statements)과 표현(expressions)이다.

for(let value of ["a", "b", "c"]){
    console.log(value);
}
// "a"
// "b"
// "c"

[..."abc"]; // ["a", "b", "c"]

function* gen(){
  yield* ["a", "b", "c"];
}

gen().next(); // { value:"a", done:false }

[a, b, c] = new Set(["a", "b", "c"]);
a // "a"

잘 정의되지 못한 iterables

만약 Iterable의 @@iterator 메소드가 iterator 객체를 반환하지 않는다면 그것은 잘 정의되지 못한 iterable이라고 할 수 있다. 이러한 iterable을 사용하는 것은 런타임 예외나 예상치 못한 결과를 불러올 수 있다:

var nonWellFormedIterable = {}
nonWellFormedIterable[Symbol.iterator] = () => 1
[...nonWellFormedIterable] // TypeError: [] is not a function

Iterator 예시

간단한 iterator

function makeIterator(array){
    var nextIndex = 0;
    
    return {
       next: function(){
           return nextIndex < array.length ?
               {value: array[nextIndex++], done: false} :
               {done: true};
       }
    };
}

var it = makeIterator(['yo', 'ya']);

console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done);  // true

무한 iterator

function idMaker(){
    var index = 0;
    
    return {
       next: function(){
           return {value: index++, done: false};
       }
    };
}

var it = idMaker();

console.log(it.next().value); // '0'
console.log(it.next().value); // '1'
console.log(it.next().value); // '2'
// ...

Generator와 함께 사용된 iterator

function* makeSimpleGenerator(array){
    var nextIndex = 0;
    
    while(nextIndex < array.length){
        yield array[nextIndex++];
    }
}

var gen = makeSimpleGenerator(['yo', 'ya']);

console.log(gen.next().value); // 'yo'
console.log(gen.next().value); // 'ya'
console.log(gen.next().done);  // true



function* idMaker(){
    var index = 0;
    while(true)
        yield index++;
}

var gen = idMaker();

console.log(gen.next().value); // '0'
console.log(gen.next().value); // '1'
console.log(gen.next().value); // '2'
// ...

generator object 는 iterator 또는 iterable 인가?

generator object 는 iterator 이면서 iterable 입니다.

var aGeneratorObject = function*(){
    yield 1;
    yield 2;
    yield 3;
}();
typeof aGeneratorObject.next;
// "function", 이것은 next 메서드를 가지고 있기 때문에 iterator입니다.
typeof aGeneratorObject[Symbol.iterator];
// "function", 이것은 @@iterator 메서드를 가지고 있기 때문에 iterable입니다.
aGeneratorObject[Symbol.iterator]() === aGeneratorObject;
// true, 이 Object의 @@iterator 메서드는 자기자신(iterator)을 리턴하는 것으로 보아 잘 정의된 iterable이라고 할 수 있습니다.
[...aGeneratorObject];
// [1, 2, 3]


기본 매개 변수 (Default Parameters)

var link = function (height, color, url) {
    var height = height || 50
    var color = color || 'red'
    var url = url || 'http://azat.co'
    ...
}

함수에 넘겨주는 인자값에 대한 default 처리를 위해 위와 같이 처리 했었다면 ES6에서는 아래와 같이 간단히 처리할 수 있다.

var link = function(height = 50, color = 'red', url = 'http://azat.co') {
  ...
}

단, 주의해야 할 점이 있다. 인자값으로 0 또는 false가 입력될 때 두 예시의 결과는 다르다. ES5에서는 || 처리 시 0 또는 false 값이 입력 되어도 거짓이 되므로 기본값으로 대체된다. 하지만 ES6의 기본 매개 변수를 사용하면 undefined 를 제외한 입력된 모든 값(0, false, null 등)을 인정한다.

Index

  1. ES6 Class 문법
  2. Class 정의
  3. constructor
  4. Prototype 기반 상속(ES5)과 Class 기반 상속(ES6) 비교
  5. super 키워드
  6. static 키워드
  7. 마치며

ES6 Class 문법

JavaScript Class는 ECMAScript 6을 통해 소개되었습니다. ES6의 Class는 기존 prototype 기반의 상속을 보다 명료하게 사용할 수 있도록 문법을 제공합니다. 이를 Syntatic Sugar라고 부르기도 합니다.

Syntatic Sugar : 읽고 표현하는것을 더 쉽게 하기 위해서 고안된 프로그래밍 언어 문법을 말합니다.

JavaScript를 ES6를 통해 처음 접하시는 분들은 알아두셔야할 것이 JavaScript의 Class는 다른 객체지향 언어(C++, C#, Java, Python, Ruby 등…)에서 사용되는 Class 문법과는 다르다는 것입니다. JavaScript에는 Class라는 개념이 없습니다.
Class가 없기 때문에 기본적으로 Class 기반의 상속도 불가능합니다. 대신 다른 언어에는 존재하지 않는 프로토타입(Prototype)이라는 것이 존재합니다. JavaScript는 이 prototype을 기반으로 상속을 흉내내도록 구현해 사용합니다. Prototype을 처음 접하시는 분은 “Prototype 이제는 이해하자”를 참고하시면 도움이 될것같습니다.


Class 정의

JavaScript에서 Class는 사실 함수입니다. 함수를 함수 선언과 함수 표현식으로 정의할 수 있듯이 class 문법도 class 선언과 class 표현식 두가지 방법으로 정의가 가능합니다.

JavaScript 엔진은 function 키워드를 만나면 Function 오브젝트를 생성하듯, class 키워드를 만나면 Class 오브젝트를 생성합니다. class는 클래스를 선언하는 키워드이고 Class 오브젝트는 엔진이 class 키워드로 생성한 오브젝트입니다.

Class 선언

함수 선언과 달리 클래스 선언은 호이스팅이 일어나지 않기 때문에, 클래스를 사용하기 위해서는 먼저 선언을 해야합니다. 그렇지 않으면 ReferenceError 가 발생합니다.

1
2
3
4
5
6
7
8
9
class People {
constructor(name) {
this.name = name;
}

say() {
console.log('My name is ' + this.name);
}
}

Class 표현식

Class 표현식은 이름을 가질 수도 있고 갖지 않을 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const People = class People {
constructor(name) {
this.name = name;
}

say() {
console.log('My name is ' + this.name);
}
}

const People = class {
constructor(name) {
this.name = name;
}

say() {
console.log('My name is ' + this.name);
}
}

constructor

constructor는 클래스 인스턴스를 생성하고 생성한 인스턴스를 초기화하는 역할을 합니다. new People() 코드를 실행하면 People.prototype.constructor가 호출됩니다. 이를 default constructor라고 하며 constructor가 없으면 인스턴스를 생성할 수 없습니다.

1
const people = new People('KimJongMin');

new People(‘KimJongMin’)을 실행하면 People 클래스에 작성한 constructor가 자동으로 호출되고 파라미터 값으로 ‘KimJongMin’을 넘겨 줍니다.

new 연산자가 인스턴스를 생성하는 것처럼 보이지만, 사실 new 연산자는 constructor를 호출하면서 파라미터를 넘겨주는 역할만 합니다. 호출된 constructor가 인스턴스를 생성하여 반환하면 new 연산자가 받아 new를 실행한 곳으로 반환합니다. 과정은 다음과 같습니다.

  1. new People(‘KimJongMin’)을 실행
  2. new 연산자가 constructor를 호출하면서 파라미터 전달
  3. constructor에 작성한 코드를 실행하기 전에 빈 Object 를 생성
  4. constructor 코드를 실행
  5. 생성한 Object(인스턴스)에 property 할당 (인스턴스를 먼저 생성했기 때문에 this로 Object 참조 가능
  6. 생성한 Object 반환

다음은 생성된 인스턴스의 구조입니다.

1
console.dir(people);

people 인스턴스의 __proto__는 People Class 오브젝트와 함께 생성된 Prototype object를 가리키고 있습니다. 결국 Class 문법을 이용한 코드를 prototype 기반의 코드로 변경하면 다음과 같습니다.

1
2
3
4
5
6
7
function People(name) {
this.name = name;
}

People.prototype.say = function () {
console.log('My name is ' + this.name);
};

Prototype 기반 상속(ES5)과 Class 기반 상속(ES6) 비교

먼저 ES5에서 Prototype을 사용하여 상속을 구현하는 방법을 살펴보고, 그 후 ES6에서 Class로 상속을 구현하는 형태를 보겠습니다.

ES5 Prototype 기반 상속

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
function Cat(name) {
this.name = name;
}

Cat.prototype.speak = function () {
console.log(this.name + ' makes a noise.');
};

function Lion(name) {
// `super()` 호출
Cat.call(this, name);
}

// `Cat` 클래스 상속
Lion.prototype = Object.create(Cat.prototype);
Lion.prototype.constructor = Lion;

// `speak()` 메서드 오버라이드
Lion.prototype.speak = function () {
Cat.prototype.speak.call(this);
console.log(this.name + ' roars.');
};

var lion = new Lion('Samba');
lion.speak();

[결과]
Sambda makes a noise.
Sambda roars.

new Lion()을 실행하면 Lion()이 호출되고, default constructor를 호출합니다. 그래서 Lion()을 생성자(constructor) 함수라고 합니다.

생성자 함수가 있으면 Cat.prototype.speak와 같이 prototype에 메서드를 연결한 코드가 있습니다. 이와 같이 prototype에 작성하지 않으면 각각의 인스턴스에 메서드가 생성되게 됩니다. 이 형태가 ES5에서 인스턴스를 구현하는 기본 형태 입니다.

Object.create()를 통해 Cat.prototype에 연결된 메서드를 Lion.prototype.__proto__에 첨부합니다. Lion.prototype에는 constructor가 연결되어 있는데 prototype을 재 할당했기 때문에 지워진 constructor를 다시 할당해 줍니다.

결과적으로 Lion 생성자 함수의 구조는 다음과 같습니다.

ES6 Class 기반 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Cat {
constructor(name) {
this.name = name;
}

speak() {
console.log(this.name + ' makes a noise.');
}
}

class Lion extends Cat {
speak() {
super.speak();
console.log(this.name + ' roars.');
}
}

const lion = new Lion('Samba');
lion.speak();

[결과]
Sambda makes a noise.
Sambda roars.

ES6에서는 extends 키워드로 상속을 구현합니다. Cat 클래스를 상속받은 Lion 클래스의 구조는 다음과 같습니다.

위의 prototype을 통해 상속을 구현한 Lion 생성자 함수의 구조와 비교했을때 일치합니다. 추가적으로 new Lion(‘Samba’) 를 실행하면 다음의 과정을 거치게됩니다.

  1. Lion 클래스의 constructor를 호출
  2. Lion 클래스에 constructor를 작성하지 않았기 때문에 슈퍼 클래스의(Cat) constructor가 호출됨 (내부적으로 프로토타입 체인으로 인해)
  3. 슈퍼 클래스의 constructor에서 this는 현재의 인스턴스를 참조하므로 인스턴스의 name 프로퍼티에 파라미터로 전달받은 값을 설정
  4. 생성한 인스턴스를 lion에 할당

super 키워드

서브 클래스와 슈퍼 클래스에 같은 이름의 메서드가 존재하면 슈퍼 클래스의 메서드는 호출되지 않습니다. 이때 super 키워드를 사용해서 슈퍼 클래스의 메서드를 호출할 수 있습니다. (서브 클래스의 constructor에 super()를 작성하면 슈퍼 클래스의 constructor가 호출됩니다.)


static 키워드

static 키워드는 클래스를 위한 정적(static) 메소드를 정의합니다. 정적 메소드는 prototype에 연결되지 않고 클래스에 직접 연결되기 때문에 클래스의 인스턴스화(instantiating) 없이 호출되며, 클래스의 인스턴스에서는 호출할 수 없습니다. 동일한 클래스 내의 다른 정적 메서드 내에서 정적 메서드를 호출하는 경우 키워드 this를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
class Lion {
static speak() {
console.log('Noise~');
}
}

Lion.speak();

[결과]
Noise~

정적 메소드는 어플리케이션(application)을 위한 유틸리티(utility) 함수를 생성하는데 주로 사용됩니다.


마치며

ES6의 Class 문법에 대해 정리해 보았다. JavaScript 언어를 약 1년전 Node.js 를 시작하며 처음 접하게 되었는데 사실 그 당시 Prototype과 상속에 대해 크게 다룰일이 없었다. (어쩌면 너무 무지해서 사용 필요성을 느끼지 못했을 수도…) 그 후 Node.js 버전을 올리고 ES6를 공부하며 Class 문법을 접하게 되었는데 JavaScript의 Prototype에 대한 이해와 지식이 부족하다 보니 이전에 공부했던 C++과 Java의 Class 처럼 이해했던 것 같다. 그래도 그 후 Prototype과 더불이 Class까지 공부하며 지금은 어느정도 이해하게 된것 같다. 결론은… 역시나 JavaScript에서 Prototype을 이해하는건 중요한것 같다.

+ Recent posts