// for..of 최초 호출 시, Symbol.iterator가 호출됩니다. [Symbol.iterator]() { // Symbol.iterator는 이터레이터 객체를 반환합니다. // for..of는 반환된 이터레이터 객체만을 대상으로 동작하는데, 이때 다음 값도 정해집니다. return { current: this.from, last: this.to,
// for..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다. next() { // next()는 객체 형태의 값, {done:.., value :...}을 반환해야 합니다. if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } }, }; }, };
// 객체 range를 대상으로 하는 이터레이션은 range.from과 range.to 사이의 숫자를 출력합니다. alert([...range]); // 1,2,3,4,5
Symbol.iterator 대신 제너레이터 함수를 사용하면, 제너레이터 함수로 반복이 가능하다.
1 2 3 4 5 6 7 8 9 10 11 12 13
let range = { from: 1, to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*()를 짧게 줄임 for (let value = this.from; value <= this.to; value++) { yield value; } }, };
alert([...range]); // 1, 2, 3, 4, 5
원래 제너레이터는 이터레이터를 어떻게 하면 쉽게 구현할지 염두에 두며 자바스크립트에 출시되었다. 제너레이터를 사용하면 이터레이터를 쓰면 복잡한 코드를 같은 기능으로 간결하게 작성할 수 있다.
제너레이터 컴포지션
제너레이터 안에 제너레이터를 임베딩(embedding, composing) 할 수 있게 해주는 제너레이터의 특별 기능이다.
연속된 숫자를 생성하는 제너레이터 함수를 만들어 본다.
1 2 3
function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; }
문자나 숫자에 적용해서 비밀번호를 생성하는 함수를 반들수 있다. 일반 함수로 구혆가ㅣ 위해서는 여러개의 함수를 만들고 그 호출 결과를 어딘가에 저장한 후 다시 그 결과들을 조합해야 원하는 기능을 구현할 수 있다. 하지만 특수 문법 yield* 를 사용하면 제너레이터를 다른 제너레이터에 끼워 넣을 수 있다.
for (let code of generatePasswordCodes()) { str += String.fromCharCode(code); }
alert(str); // 0..9A..Za..z
yield* 지시자는 실행을 다른 제너레이터에 위임한다. 제너레이터를 대상으로 반복을 수행하고, 산출 값들을 바깥으로 전달한다는 것을 의미한다.
yield 를 사용해 제너레이터 안, 밖으로 정보 교환하기
제너레이터의 yield는 양방향 길과 같은 역할을 한다. yield 는 결과를 바깥으로 전달할 뿐만 아니라 값을 제너레이터 안으로 전달하기까지 한다. 값을 안, 밖으로 전달하려면 generate.next(arg) 를 호출해야 한다. 이때 인수 arg는 yield의 결과가 된다.
1 2 3 4 5 6 7 8 9 10 11 12
function* gen() { // 질문을 제너레이터 밖 코드에 던지고 답을 기다립니다. let result = yield"2 + 2 = ?"; // (*)
alert(result); }
let generator = gen();
let question = generator.next().value; // <-- yield는 value를 반환합니다.
generator.next(4); // --> 결과를 제너레이터 안으로 전달합니다.
generator.next()를 처음 호출할 댄 항상 인수가 없어야 한다. 인수가 들어오면 무시함 이때 yield "2+2=?"의 결과가 반환된다.
변수 question에 이 값이 할당된다.
generator.next(4) 를 호출하면 인수 4 가 result에 할당된다.
제너레이터와 외부 호출코드는 next/yield를 이용해 결과를 전달 및 교환한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
function* gen() { let ask1 = yield"2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield"3 * 3 = ?";
alert(ask2); // 9 }
let generator = gen();
alert(generator.next().value); // "2 + 2 = ?"
alert(generator.next(4).value); // "3 * 3 = ?"
alert(generator.next(9).done); // true
코드를 실행하면 차례대로 다음과 같이 출력된다.
2 + 2 = ?
4
3 * 3 = ?
9
true
generator.throw
generator 외부 코드가 에러를 만들거나 던질 수도 있다. 에러를 yield 안으로 전달하려면 generator.throw(err)를 호출해야 한다. 호출하면 err는 yield 가 있는 줄로 던져진다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
function* gen() { try { let result = yield"2 + 2 = ?"; // (1)
alert( "위에서 에러가 던져졌기 때문에 실행 흐름은 여기까지 다다르지 못합니다." ); } catch (e) { alert(e); // 에러 출력 } }
let generator = gen();
let question = generator.next().value;
generator.throw(newError("데이터베이스에서 답을 찾지 못했습니다.")); // (2)
(2)에서 던진 에러는 (1) 에서 예외를 만든다. 만약 제너레이터 안에서 에러를 처리 하지 않았다면 외부로 떨어져 나온다. 바깥에서 에러를 처리해도 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13
function* generate() { let result = yield"2 + 2 = ?"; // Error in this line }
let generator = generate();
let question = generator.next().value;
try { generator.throw(newError("데이터베이스에서 답을 찾지 못했습니다.")); } catch (e) { alert(e); // 에러 출력 }
// for..of 최초 실행 시, Symbol.iterator가 호출됩니다. [Symbol.iterator]() { // Symbol.iterator메서드는 이터레이터 객체를 반환합니다. // 이후 for..of는 반환된 이터레이터 객체만을 대상으로 동작하는데, // 다음 값은 next()에서 정해집니다. return { current: this.from, last: this.to,
// for..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다. next() { // (2) // next()는 객체 형태의 값, {done:.., value :...}를 반환합니다. if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } }, }; }, };
for (let value of range) { alert(value); // 1, 2, 3, 4, 5 }
이터러블 객체를 비동기적으로 만들기 위한 작업
Symbol.iterator 대신, Symbol.asyncIterator를 사용해야 한다.
next() 는 프라미스를 반환해야 한다.
비동기 이터러블 객체를 대상으로 하는 반복 작업은 for await (let item of iterable) 반복문을 사용해 처리해야 한다.
// for await..of 최초 실행 시, Symbol.asyncIterator가 호출됩니다. [Symbol.asyncIterator]() { // (1) // Symbol.asyncIterator 메서드는 이터레이터 객체를 반환합니다. // 이후 for await..of는 반환된 이터레이터 객체만을 대상으로 동작하는데, // 다음 값은 next()에서 정해집니다. return { current: this.from, last: this.to,
// for await..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다. asyncnext() { // (2) // next()는 객체 형태의 값, {done:.., value :...}를 반환합니다. // (객체는 async에 의해 자동으로 프라미스로 감싸집니다.)
// 비동기로 무언가를 하기 위해 await를 사용할 수 있습니다. awaitnewPromise((resolve) =>setTimeout(resolve, 1000)); // (3)
(async () => { forawait (let value of range) { // (4) alert(value); // 1,2,3,4,5 } })();
일반 이터레이터와, async 이터레이터에 차이점
객체를 비동기적으로 반복 가능하도록 하려면, Symbol.asyncIterator메서드가 반드시 구현되어야 함.
Symbol.asyncIterator는 프라미스를 반환하는 next()가 구현된 객체를 반환해야 한다.
next() 는 aync메서드일 필요는 없지만 await를 쓰면 편하기 때문에 편의상 이렇게 사용한다.
반복 작업을 하려면 for await를 붙인다. 그럼 range[Symbol.asyncIterator] ()가 일회 호출되는데, 그 이후엔 각 값을 대상으로 next()가 호출된다.
전개 문법 ...은 동기적으로 동작하지 않는다. 동기 작업이 필요한 이터레이터는 Symbol.asyncIterator가 아닌 Symbol.iterator을 찾기 때문이다.
async 제너레이터
일반 제너레이터
1 2 3 4 5 6 7 8 9
function* generateSequence(start, end) { for (let i = start; i <= end; i++) { yield i; } }
for (let value of generateSequence(1, 5)) { alert(value); // 1, then 2, then 3, then 4, then 5 }
async를 붙여주면 비동기적으로 제너레이터를 사용할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
asyncfunction* generateSequence(start, end) { for (let i = start; i <= end; i++) { // await를 사용할 수 있습니다! awaitnewPromise((resolve) =>setTimeout(resolve, 1000));
yield i; } }
(async () => { let generator = generateSequence(1, 5); forawait (let value of generator) { alert(value); // 1, 2, 3, 4, 5 } })();
aync 제너레이의 generator.next()의 메서드는 비동기적이 되고 프라미스를 반환한다. 따라서 await 키워드를 사용해서 generator.next()를 사용하도록 한다.
1
result = await generator.next(); // result = {value: ..., done: true/false}
async 이터러블
반복 가능한 객체를 만들려면 객체에 Symbol.iterator를 추가해야 한다.
1 2 3 4 5 6 7
let range = { from: 1, to: 5, [Symbol.iterator]() { return<range를 반복가능하게 만드는 next가 구현된 객체> } }
일반적으로 제너레이터를 반환하도록 구현하는 경우가 더 많다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
let range = { from: 1, to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*()를 짧게 줄임 for (let value = this.from; value <= this.to; value++) { yield value; } }, };
for (let value of range) { alert(value); // 1, 2, 3, 4, 5 }
지금 이 상태에서 비동기 동작을 축하려면, Symbol.iterator를 async Symbol.asyncIterator로 바꿔야 한다.
async *[Symbol.asyncIterator]() { // [Symbol.asyncIterator]: async function*()와 동일 for (let value = this.from; value <= this.to; value++) { // 값 사이 사이에 약간의 공백을 줌 awaitnewPromise((resolve) =>setTimeout(resolve, 1000));
yield value; } }, };
(async () => { forawait (let value of range) { alert(value); // 1, 2, 3, 4, 5 } })();
default export 는 꼭 파일내에서 내보내는 이름으로 불러오지 않아도 되기 때문에 같은 모듈이라도 사용하는 사람에 따라서 불러온 이름이 다를 수 있다. 이는 혼란을 야기할 수 있는데 이것을 방지하기 위해서 이름을 정하는 규칙이 있다. 보통 파일이름에 첫 글자는 대문자인 형식으로 사용한다.
모듈 다시 내보내기
1 2 3
export { sayHi } from"./say.js"; // sayHi를 다시 내보냄
export { defaultas User } from"./user.js"; // default export 를 다시 내보내기 함.
가져온 객체를 즉시 다시 내보내기 할수 있다. 내보낼 기능을 패키지 전반에 분산하여 구현한 후 필요한 파일에서 가져온후 다시 내보내는 방식으로 활용할 수 있다.
default export 다시 내보내기
1 2 3 4
// user.js exportdefaultclassUser{ //... }
User를 export User from './user.js 로 다시 내보내기 할 때 문법 에러가 발생한다. export {default as User}를 사용해야 한다.
export * from './user.js'를 사용했다면 dafault export는 무시 되고 named export 만 다시 내보내진다.
동시에 하기 위해서는 다음과 같이 사용해야 한다.
1 2
export * from"./user.js"; // named export를 다시 내보내기 export { default } from"./user.js"; // default export 다시 내보내기
Object.keys는 enumerable 플래그가 있는 프로퍼티만 반환한다. ownKeys에서 enumerable 플래그가 없는 객체를 반환했다면, Object.keys를 호출해도 빈 문자열이 반환된다.
이럴땐 getOwnPropertyDescriptor를 사용하면 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
let user = {};
user = newProxy(user, { ownKeys(target) { // 프로퍼티 리스트를 받을 때 딱 한번 호출된다. return ['a', 'b', 'c']; }
getOwnPropertyDescriptor(target, prop) { // 모든 프로퍼티를 대상으로 호출된다. return { enumerable: true, configurable: true, // 이 외의 플래그도 반환할 수 있다. "value:..."도 가능하다. } } })
alert(Object.keys(user)); // a, b, c
deleteProperty 와 여러 트랩을 사용해 프로퍼티 보호하기
_가 붙은 프로퍼티는 내부에서만 사용하는 프로퍼티를 의미한다. 외부에서 접근해서는 안되는데 기술적으로 접근 가능한 문제가 있다.
프록시를 이요해서 접근하지 못하도록 막아 본다.
get: 프로퍼티를 읽으려고 하면 에러를 던져줌
set: 포로퍼티에 쓰려고 하면 에러를 던져줌
deleteProperty: 프로퍼티를 지우려고 하면 에러를 던져줌
ownKeys: for..in 이나 Object.keys같은 프로퍼티 순환 메서드를 사용할 때 _로 시작하는 메서드는 제외함.
(user.hi)()는 정상 작동한다. 이는 (expression)() 형태에서는 참조 값이 아닌 함수 값 자체가 전달되기 때문이다.
참조 타입 자세히 알아보기
obj.method() 엔 연사이 두개 있다.
. 은 객체 프로퍼티 obj.method에 접근한다.
괄호 ()는 점근한 프로퍼티를 실행한다.
첫번째 연산에서 얻은 this가 어떻게 두번째 연산으로 전달 될까?
1 2 3 4 5 6 7 8 9 10
let user = { nsme: "John", hi() { alert(this.name); }, };
// 메서드 접근과 호출을 별도의 줄에서 let hi = user.hi; hi(); // this 가 undefined이기 때문에 에러가 발생한다.
자바스크립트에서 user.hi()를 의도한대로 동작 시키기 위해서 .은 함수를 반환 시키는 것이 아니라 참조 타입을 반환하게 된다. 참조 타입에 속하는 값은 (base, name, strict) 이 조합된 형태이다.
base: 객체
name: 프로퍼티 이름
strict: 엄격 모드에서 true
user.hi()로 프로퍼티에 접근하면 함수가 아닌, 참조형(참조 타입) 값을 반환한다. 엄격 모드에선 아래와 같이 반환된다.
1 2
// 참조형 값 user, "hi", true;
참조형 값에 ()를 붙여 호출하면 객체, 객체의 메서드와 연관된 모든 정보를 얻는다. 이 정보를 기반으로 this(=user) 가 결정된다.
참조 타입은 . 연산에서 알아낸 정보를 괄호 ()로 전달해 주는 중개인 역할을 한다.
()없이 user.hi를 호출하면 점연산이 아닌 연산이 된다(할당 연산). 이때는 참조 값이 아니라 함수 만 받아서 전달하기 때문에 this정보가 사라진다. obj.method()같이 점을 사용하거나, obj[method]() 같이 대괄호를 사용해 함수를 호출했을 때만 this 값이 의도한대로 전달된다. func.bind() 등을 이용하면 해결 하는 방법도 있긴 한다.