ES6에서 도입된 제너레이터는 코드블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수다.
1. 제너레이터 함수는 함수 호출자에게 함수 실행의 제어권을 양도할 수 있다.
: 일반 함수를 호출하면 제어권이 함수에게 넘어가고 함수 코드를 일괄 실행한다. 제너레이터 함수는 함수 실행을 함수 호출자가 일시 중지시키거나 재개시킬 수 있다.함수의 제어권을 함수가 독점하는 것이 아니라 함수 호출자에게 양도할 수 있다는 것을 의미한다
.
2. 제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다.
: 일반 함수는 함수가 실행되고 있는 동안에는 함수 외부에서 함수 내부로 값을 전달하여 함수의 상태를 변경할 수 없다. 제너레이터 함수는 함수 호출자에게 상태를 전달할 수 있고, 함수 호출자로부터 상태를 전달받을 수도 있다.
3. 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
: 제너레이터 함수를 호출하면 함수 코드를 실행하는 것이 아니라, 이터러블이면서 동시에 이터레이터인 제너레이터 객체를 반환한다.
function* 키워드로 선언한다. 그리고 하나 이상의 yield 표현식을 포함한다.
// 제너레이터 함수 선언문
function* genDecFunc() {
yield 1;
}
// 제너레이터 함수 표현식
const genExpFunc = function* () {
yield 1;
}
// 제너레이터 메서드
const obj = {
* genObjMethod() {
yield 1;
}
};
⛔️ 제너레이터 함수는 화살표 함수로 정의할 수 없다.
⛔️ 제너레이터 함수는 new 연산자와 함께 생성자 함수로 호출할 수 없다.
제너레이터 함수를 호출하면 일반 함수처럼 함수 코드블록을 실행하지 않고,
제너레이터 객체를 생성해 반환한다.
- 제너레이터 함수가 반환한 제너레이터 객체는 이터러블이면서 동시에 이터레이터다.
다시 말해, Symbol.iterator메서드를 상속받는 이터러블이면서 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환하는 next 메서드를 소유하는 이터레이터다.
function* getFunc() {
yield 1;
yield 2;
yield 3;
}
// 제너레이터 함수 호출시 제너레이터 객체를 반환
const generator = getFunc();
console.log(Symbol.iterator in generator); //true
console.log('next' in generator) // true
제너레이터 객체는 next 메서드를 갖는 이터레이터이지만, 이터레이터에는 없는 return, throw 메서드를 갖는다. 세 개의 메서드를 호출하면 다음과 같이 동작한다.
function* genFunc(){
try{
yield 1;
yield 2;
yield 3;
} catch(e){
console.error(e);
}
}
const generator = genFunc();
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 키워드는 제너레이터 함수의 실행을 일시 중지시키거나 yield 키워드 뒤에 오는 표현식의 평가 결과를 제너레이터 함수 호출자에게 반환한다.
// 제너레이터 함수
function getFunc(){
yield 1;
yield 2;
yield 3;
}
const generator = getFunc();
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: undefined, done:true}
제너레이터 객체의 next 메서드는 이터레이터의 next 메서드와 달리 인수를 전달할 수 있다. 제너레이터 객체의 next 메서드에 전달한 인수는 제너레이터 함수의 yield 표현식을 할당받는 변수에 할당된다.
⛔️ yield 표현식을 할당받는 변수에 yield 표현식의 평가 결과가 할당되지 않는것을 주의해야 한다!!
function* getFunc(){
// 처음 next 메서드를 호출하면 첫 번째 yield 표현식까지 실행되고 일시 중지된다.
// x 변수에는 아직 아무것도 할당되지 않았다. x 변수의 값은 next 메서드가 두 번째 호출될 때 결정된다.
const x = yield 1;
// 인수 10은 첫 번째 yield 표현식을 할당받는 x 변수에 할당된다.
const y = yield (x + 10);
return x + y;
}
const generator = getFunc(0);
let res = generator.next();
console.log(res); // {value: 1, done: false}
res = generator.next(10); // 10은 이 때 x에 할당된다.
console.log(res); // {value: 20, done: false}
res = generator.next(20);
console.log(res); // {value: 30, done:false}
😆 이처럼 제러네이터 함수는 next 메서드와 yield 표현식을 통해 함수 호출자와 함수의 상태를 주고받을 수 있다. 함수 호출자는 next 메서드를 통해 yield 표현식까지 함수를 실행시켜 제너레이터 객체가 관리하는 상태를 꺼내올 수 있고,, next 메서드에 인수를 전달해서 제너레이터 객체에 상태를 밀어넣을 수 있다. 이를 잘 활용하면 비동기처리를 동기처리처럼 구현할 수 있다.
기존에 무한 이터러블 생성하는 함수
const infiniteFibonacci = (function () {
let [pre, cur] = [0,1];
return {
[Symbol.iterator]() {return this;},
next() {
[pre, cur] = [cur, pre+cur];
// 무한 이터러블이므로 done 프로퍼티를 생략
return {value: cur};
};
};
}()};
제너레이터를 활용하면 손쉽게 만들 수 있다.
const infiniteFibonacci = (function* () {
let [pre, cur] = [0, 1];
while(true) {
[pre, cur] = [cur, pre+cur];
yield cur;
}
}()};
for (const num of infiniteFibonacci){
if(num>10000) break;
console.log(num); // 1 2 3 5 8 ...
}
제너레이터 함수는 next 메서드와 yield 표현식을 통해 함수 호출자와 함수의 상태를 주고받을 수 있다고 했다. 이러한 특성을 활용하면 프로미스를 사용한 비동기 처리를 동기 처리처럼 구현할 수 있다. 다시 말해, 프로미스의 후속 처리 메서드 then/catch/finally 없이 비동기 처리 결과를 반환하도록 구현할 수 있다.
const async = generatorFunc => {
const generator = generatorFunc(); // --- 2) 제너레이터 객체를 생성하고
const onResolved = arg => {
const result = generator.next(arg); // ---5) next메서드를 호출한다.
return result.done ? result.value : result.value.then(res => onResolved(res)); // --- 7) 재귀적으로 onResolved 함수를 다시 실행
};
return onResolved; // --- 3) onResolved함수를 반환한다.
};
(async(function* fetchTodo(){ // --- 1) async함수 호출
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const response = yield fetch(url); // --- 6) fetch까지 실행, done이 false이므로
const todo = yield response.json(); // ---7) response에 값이 할당되고, yield까지 또 실행
console.log(todo);
})()); // ---4) onResolved 함수를 즉시실행하고
*async 함수는 이해를 돕기 위해 간략화 한 것이다.
제너레이터 이용해서 비동기 처리 가능해졌으나 코드가 복잡해졌다. ES8에서는 제너레이터보다 간단하고 가독성 좋게 비동기 처리를 동기 처리처럼 동작하도록 구현할 수 있는 async/await가 도입되었다.
async/await는 프로미스를 기반으로 동작하며, then/catch/finally 후속 처리 메서드에 콜백함수를 전달해서 비동기 처리 결과를 후속 처리할 필요 없이 마치 동기 처리처럼 프로미스를 사용할 수 있다.
async function fetchTodo() {
const url = 'http~~';
const reponse = await fetch(url);
const todo = await response.json();
console.log(todo);
await 키워드는 반드시 async 함수 내부에서 사용해야 한다.
async 함수는 async 키워드를 사용해 정의하며 언제나 프로미스를 반환한다.
async 함수가 명시적으로 프로미스를 반환하지 않더라도 async 함수는 암묵적으로 반환값을 resolve하는 프로미스를 반환한다.
// async 함수 선언문
async funcction foo(n) { return n; }
foo(1).then(v => console.log(v));// 1
// async 함수 표현식
const bar = async function (n) {return n;};
// async 화살표 함수
const bar = async n => n;
await 키워드는 프로미스가 settled 상태가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve 한 처리 결과를 반환한다. await 키워드는 반드시 프로미스 앞에서 사용해야 한다.
async function foo() {
const a = await new Promise(resolve => setTimeout(() => resolve(1), 3000));
const b = await new Promise(resolve => setTimeout(() => resolve(2), 2000));
const c = await new Promise(resolve => setTimeout(() => resolve(3), 1000));
}
// 아무 연관이 없는 위의 애들을 저렇게 처리해 줄 필요가 없다.
// 아래처럼 Promise.all 로 한번에 묶어서 처리하는게 낫다.
async function foo() {
const res = await Promise.all([new Promise(resolve => setTimeout(() => resolve(1), 3000)),
new Promise(resolve => setTimeout(() => resolve(2), 2000)),
new Promise(resolve => setTimeout(() => resolve(3), 1000))]);
}
비동기 처리를 위한 콜백 패턴의 단점 중 가장 심각한 것은 에러처리가 곤란하다는 것이었다. 비동기함수의 콜백함수를 호출한 것은 비동기 함수가 아니기 때문에 try...catch문을 사용해 에러를 캐치할 수 없다.
그러나 async/await 에서 에러처리는 try...catch문을 사용할 수 있다. 콜백 함수를 인수로 전달받는 비동기 함수와는 달리 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확하다.
const foo = async() => {
try{
const wrongUrl= 'https://wrong.url';
const response = await fetch(wrongUrl);
const data = await response.json();
}catch(err){
console.log(err); // try 코드 블록 내의 모든 문에서 발생한 일반적인 에러까지 모두 캐치 가능
}
};
foo().then(console.log).catch(console.error); // 요렇게도 사용가능하다.