ES6에서 도입된 제너레이터는 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수다. 제너레이터와 일반 함수의 차이는 다음과 같다.
제너레이터 함수는 함수 호출자에게 함수 실행의 제어권을 양도할 수 있다.
다시 말해, 함수 호출자가 함수 실행을 일시 중지시키거나 재개시킬 수 있다. 이는 함수의 제어권을 함수가 독점하는 것이 아니라 함수 호출자에게 양도(yield)할 수 있다는 것을 의미한다.
제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다.
일반 함수의 경우에 함수가 실행되고 있는 동안에는 함수 외부에서 함수 내부로 값을 전달하여 함수의 상태를 변경할 수 없다. 하지만 제너레이터 함수는 함수 호출자에게 상태를 전달할 수 있고 함수 호출자로부터 상태를 전달받을 수도 있다.
제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
제너레이터 함수를 호출하면 함수 코드를 실행하는 것이 아니라 이터러블이면서 동시에 이터레이터인 제너레이터 객체를 반환한다.
제너레이터 함수는 function* 키워드로 선언한다. 그리고 하나 이상의 yield 표현식을 포함한다.
이것을 제외하면 일반 함수를 정의하는 방법과 같으나 화살표 함수로 정의할 수 없으며 new 연산자와 함께 생성자 함수로 호출할 수 없다. 애스터리스크(*)의 위치는 function 키워드와 함수 이름 사이라면 어디든지 상관없다. 하지만 일관성을 위해 function 키워드 바로 뒤에 붙이는 것을 권장한다.
function* getFunc() { yield 1;} //이 방식을 권장함
function * getFunc() { yield 1;}
제너레이터 함수를 호출하면 일반 함수처럼 함수 코드 블록을 실행하는 것이 아니라 제너레이터 객체를 생성해 반환한다. 제너레이터 함수가 반환한 객체는 이터러블하면서(Symbol.iterator 메서드를 상속받음) 이터레이터(next 메서드를 갖는다)이다. 제너레이터는 next 메서드를 갖는 이터레이터면서 return, throw 메서드를 갖는다.
function* getFunc() {
yield 1;
yield 2;
yield 3;
}
const generator = getFunc();
console.log(Symbol.iterator in generator); //true
console.log('next' in generator); //true;
function* getFunc() {
try {
yield 1;
yield 2;
yield 3;
} catch(e) {
console.error(e);
}
}
//제너레이터 함수를 호출하면 제너레이터 객체를 반환한다. 즉 아래 변수의 값은 제너레이터 객체이다.
const generator = getFunc();
console.log(generator.next()); //{value: 1, done: false}
console.log(generator.return('End!')); //{value: "End!", done: true}
console.log(generator.throw('Error!')); //{value: undefined, done: true}
제너레이터는 yield 키워드와 next 메서드를 통해 실행을 중지했다가 필요한 시점에 다시 재개할 수 있다. 제너레이터 함수를 호출하면 제너레이터 함수의 객체를 반환하는데 이 함수는 객체의 메서드를 사용하여 제너레이터 함수의 코드 블록을 제어할 수 있다.
yield 키워드는 제너레이터 함수의 실행을 중지시키더나 yield 키워드 뒤에 오는 표현식의 평과 결과를 제너레이터 함수 호출자에게 반환한다.
제너레이터 객체의 next 메서드를 호출하면 yield 표현식까지 실행되고 일시 중지(suspend)
된다. 이때 함수의 제어권이 호출자로 양도(yield)된다. 이후 필요한 시점에 호출자가 다시 next 메서드를 호출하면 일시 중지된 코드부터 실행을 재개(resume)하기 시작하여 다음 yield 표현식까지 실행되고 또 다시 일시 중지된다.
이터레이터의 next 메서드와 달리 제너레이터 객체의 next 메서드에는 인수를 전달할 수 있다. 제너레이터 객체의 next 메서드에 전달한 인수는 제너레이터 함수의 yield 표현식을 할당받는 변수에 할당된다.
function* getFunc() {
//첫 번째 next 메서드가 실행됐을 때 첫 번째 yield 표현식까지 실행되고 일시 중지된다.
//이 때 변수 x에는 아무것도 할당되지 않은 상태로 다음 next 메서드가 실행될 때 결정된다.
const x = yield 1;
//두 번째 next 메서드를 실행할 때 전달된 인수는 첫 번째 yield 표현식을 할당받는 x에 할당된다.
//즉, const x = yield 1;는 두 번째 next 메서드를 호출했을 때 완료된다.
//그리고 두 번째 next 메서드에 10 + 10이 이터레이터 반환 객체의 value 프로퍼티에 할당된다.
const y = yield (x + 10);
//일반적으로 제너레이터의 반환값은 의미가 없다.
//보통 제너레이터에서는 값을 반환할 필요가 없고 return은 종료의 의미로만 사용해야 한다.
return x + y;
}
const generator = getFunc();
let res = generator.next();
console.log(res); //{value: 1, done: false}
res = generator.next(10);
console.log(res); //{value: 20, done: false}
이터러블의 구현
: 제너레이터를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간단히 이터러블을 구현할 수 있다.
비동기 처리
: 프로미스의 후속 처리 메서드 then/catch/finally 없이 비동기 처리 결과를 반환하도록 구현할 수 있다.
제너레이터로 비동기 처리를 동기 처리처럼 동작하도록 구현할 수는 있지만 코드가 길고 가독성이 나쁘다는 단점이 있었다. 그래서 ES8에서는 더 간단하고 가독성 좋게 비동기 처리를 동기 처리처럼 동작하도록 구현할 수 있는 async/await가 도입되었다.
async/await는 프로미스를 기반으로 동작한다. 대신 프로미스의 then/catch/finally 메서드를 사용하지 않고 쉽게 후속처리가 가능하다.
async 함수
: await 키워드는 반드시 async 함수 내부에서 사용해야 한다. async 함수는 async 키워드를 사용해 정의하며 언제나 프로미스를 반환한다. 다만 클래스의 constructor 메서드는 async 함수로 만들 수 없다. constructor 메서드는 인스턴스를 반환해야 하지만 async 함수는 언제나 프로미스를 반환해야 하기 때문이다.
await 키워드
: await 키워드는 프로미스가 settled 상태가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리 결과를 반환한다. await는 반드시 프로미스 앞에서 사용해야 한다. 하지만 모든 프로미스에 await 키워드를 사용하는 것은 주의해야 한다.
async 함수 안에서 서로 연관없이 개별적으로 수행되는 비동기 함수가 여러개일 경우에는 Promise.all로 함수를 묶고 그 앞에 한 번만 await 키워드를 사용하는 것이 더 효율적이다.
async function foo() {
const res = await Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)),
new Promise(resolve => setTimeout(() => resolve(2), 1000)),
new Promise(resolve => setTimeout(() => resolve(3), 2000)),
])
console.log(res); //[1, 2, 3]
}
foo(); //약 3초 소요됨, 각각 함수에 await 키워드를 붙여 처리했다면 약 6초가 소요됐을 것이다.
출처: 모던 자바스크립트 Deep Dive-이웅모