제너레이터는 코드 블록의 실행을 일시 중지 했다가 필요한 시점에 재개 할 수 있는 특수한 함수이다.
생각해보면 일반적으로 함수가 호출 된 순간 함수의 동작은 우리가 제어 할 수 없다.
function countNum() {
console.log(1);
console.log(2);
console.log(3);
}
countNum(); // 1 2 3
countNum 은 호출되는 순간 엔진에 의해 1,2,3 을 순차적으로 호출한다.
하지만 제너레이터를 이용하면 함수를 호출하더라도 함수의 실행 시점을 마음대로 제어 할 수 있다.
function* generatorCountNum() {
yield 1;
yield 2;
yield 3;
}
const generator = generatorCountNum();
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log('흠.. 다음꺼를 호출할까 말까 .. '); // 흠.. 다음꺼를 호출할까 말까 ..
console.log(generator.next().value); // 3
그 이유는 이터레이터와 큰 연관이 있다.
또한 함수는 함수 호출자와 상태를 주고 받을 수 없다.
함수는 인수들을 호출 시에 전달 받고 전달 받은 상태에서 호출 된 이후로는 외부의 값을 변경 시키기만 할 수 있을 뿐 새로운 값을 호출되는 도중 전달 받을 수 없다.
하지만 제네레이터는 가능하다.
function* geneFunc() {
const x = yield 'x값을 넣는다요';
const y = yield 'y값을 넣는다요';
yield x + y;
}
const generator = geneFunc();
console.log(generator.next()); // { value: 'x값을 넣는다요', done: false }
console.log(generator.next(10)); // { value: 'y값을 넣는다요', done: false }
console.log(generator.next(20)); // { value: 30, done: false }
console.log(generator.next()); // { value: undefined, done: true }
함수를 호출 한 후 함수 외부에서 값을 넣어주었다.
이것이 가능한 이유는
제네레이터 함수를 호출하면 이터러블이면서 동시에 이터레이터인 제네레이터 객체를 반환하기 때문이다.
next를 사용하는 것을 보면서 이터레이터가 사용되었음을 짐작 할 수 있었을 것이다.
제네레이터에 대해 공부하기전에 이터레이터를 복습해보자
이터레이터는 Symbol 의 메소드로 , 어떤 객체가 [Symbol.iterator] 메소드를 가지고 있다면 해당 객체는 이터러블 한 객체이며, 반복문에서 이터러블한 객체를 사용하게 되면 [Symbol.iterator] 메소드가 실행되어 이터레이터 객체 를 반환한다.
const iteratorFibo = () => {
let [pre, cur] = [0, 1];
return {
[Symbol.iterator]() {
return this;
},
next() {
[pre, cur] = [cur, pre + cur];
return { value: cur, done: false };
},
};
};
const fibo = iteratorFibo();
console.log(fibo);
/*
{
next: [Function: next],
[Symbol(Symbol.iterator)]: [Function: [Symbol.iterator]]
}
*/
console.log(fibo.next()); // { value: 1, done: false }
console.log(fibo.next()); // { value: 2, done: false }
console.log(fibo.next()); // { value: 3, done: false }
console.log(fibo.next()); // { value: 5, done: false }
console.log(fibo.next()); //{ value: 8, done: false }
맨 처음 반환된 fibo 객체는 이터러블 한 객체이다.
이터러블 한 객체라는 것은 내부 메소드로 [Symbol.iterator] 를 가지고 있으며 next 메소드가 존재하는 객체이다.
next 메소드가 호출 될 때 마다 next 에서 정의된 코드 블록이 실행되며 값이 반환된다.
반환값은 {value , num } 프로퍼티를 갖는다.
이터레이터는 반복문으로 사용되면 value 값을 반환한다.
const iteratorFibo = () => {
let [pre, cur] = [0, 1];
return {
[Symbol.iterator]() {
return this;
},
next() {
[pre, cur] = [cur, pre + cur];
return { value: cur, done: false };
},
};
};
const fibo = iteratorFibo();
for (const num of fibo) {
if (num > 100) break;
console.log(num); // 1 2 3 5 ... 34 55 89
}
정리하자면 이터레이터는 {value , done} 값을 반환하는 객체이며 next() 메소드를 가지고 있다. next() 메소드는 실행 될 때 마다 {value , done} 값을 반환하며 done 프로퍼티가 true 인 경우 반복이 중단된다.
이터러블 한 객체는 이터레이터 객체를 반환하는 메소드인 [Symbol.iterator] 를 내부 메소드로 가지고 있는 객체를 의미한다.
정리
그러면 제너레이터 는 이터러블이면서 동시에 이터레이터인 제너레이터 객체를 반환한다는 것이 어떤 의미인지 대충 짐작이 간다.
제너레이터 객체는next메소드와[Symbol.iterator]메소드를 가지고 있으며 값을 반환하는구나근데 반환하는 양식이 조금 다르다.
yield라는 개념이 사용된다.
제너레이터 함수는 function* 키워드로 선언한다. 그리고 하나 이상의 yield 표현식을 포함한다.
이것을 제외하곤 일반 함수와 정의법이 같다.
??? : "팡숀? 웬만하면 사용하지 마세요 . 화살표 함수와 메소드를 이용하세요"
그랬지만 제너레이터는
function으로 정의해야 한다. 화살표 함수로는 정의 할 수 없다.
* 에스터리스크의 위치는 함수와 키워드 사이라면 어디든 상관없다. 근데 일관성 유지를 위해 function* 을 추천한다.
function* 함수명,function *함수명모두 가능
const generator = (function* geneFunc() {
yield 1;
yield 2;
yield 3;
})();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
제너레이터 객체는 이터레이터를 상속받기 때문에 next() 메소드를 가지고 있고 {value ,done} 인 이터레이터 리절트 객체를 반환한다.
추가로 이터레이터에는 없는 return , throw 메소드를 가지고 있다.
console.log(generator.return('End!')); // { value: 'End!', done: true }
return 메소드는 전달받은 인수를 value 로 , done 을 true 로 한 이터레이터 리절트 객체를 반환한다.
throw 메소드는 전달받은 인수를 에러명으로 하여 에러를 생성하고 {value : undefined done : true} 인 이터레이터 리절트 객체를 반환한다.
제너레이터는 이터레이터면서 제너레이터라고 했는데 .. 그럼 저기 보이는 yield 는 뭘까 ?

사전적 의미는 이런 느낌인데 .. 코드에서는 양도 하다 이런 느낌으로 쓰인다.
제너레이터의 next() 메소드가 실행 됐을 때 yield 가 있는데 걔는 뭘까 ? return 하고 뭐가 다를까?
yield 는 next 메소드가 실행 됐을 때의 중단점과 같다.
function* geneFunc() {
console.log('첫 번째 넥스트 실행합니다');
yield 1;
console.log('두 번째 넥스트 실행합니다');
yield 2;
console.log('세 번째 넥스트 실행합니다');
yield 3;
console.log('끝났습니다');
}
const generator = geneFunc();
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 메소드를 실행 할 때 제너레이터는 yield 를 만나기 전까지의 코드블록을 실행하고 yield 에 있는 값을 반환한다.
그러니 제너레이터 객체의 next 메소드를 실행하면 yield 문 전까지 실행 후 yield 의 값을 이터레이터 리절트 객체 에 담아 반환하고 일시 중지(suspend) 된다. 이 때 함수의 제어권이 호출자로 양도(yield) 된다.
일시 중지한 함수의 호출은 이제 함수 호출자 (위 예시에선
generator)가next메소드를 이용해 제어 할 수 있다.
이 때 next() 메소드에는 인수를 전달 할 수 있는데 전달한 인수는 yield 표현식을 할당받는 변수에게 할당된다.
function* geneFunc() {
const x = yield 'x 값 설정';
const y = yield 'y 값 설정';
yield x + y;
}
const generator = geneFunc();
console.log(generator.next()); // { value: 'x 값 설정', done: false }
console.log(generator.next(1)); // 전달 받은 인수를 x 에다가 설정
// { value: 'y 값 설정', done: false }
console.log(generator.next(2)); // 전달 받은 인수를 y 에다가 설정
// { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
순서가 좀 헷갈릴 수 있지만 변수에 값이 할당 되는 순서를 살펴보면 헷갈리지 않는다.
const x = yield 'x 값 설정'에서x에 값을 할당 할 때 우선 우항인yield 'x값 설정'표현식을 평가한다.평가된 표현식은
yield가 있으니suspend되고'x값 설정'이란 문구 리터럴을 이터레이터 리절트 객체에 담아 반환한다.
현재x에 담긴 값은undefined이다.그 다음
next(1)으로suspended된 상태의 제너레이터를 실행하면 전달받은 인수를const x값에 할당한다.
제너레이터는 이터레이터를 상속 받은 객체이기 때문에 이터레이터와 거의 유사하게 사용 될 수 있다.
const fibo = (function* generatorFibo() {
let [pre, cur] = [0, 1];
while (true) {
[pre, cur] = [cur, pre + cur];
yield cur;
}
})();
for (const num of fibo) {
if (num > 100) break;
console.log(num);
}
만약 해당 피보나치를 이터레이터 객체를 생성해서 만들었으면 다음과 같다.
const fibo = (() => {
let [pre, cur] = [0, 1];
return {
[Symbol.iterator]() {
return this;
},
next() {
[pre, cur] = [cur, pre + cur];
return { value: cur, done: false };
},
};
})();
for (const num of fibo) {
if (num > 100) break;
console.log(num);
}
우우우 ~~ 제너레이터 구우웃 ㅋㅋ
근데 제너레이터가 가장 쓸모있게 쓰이는 부분은 바로 비동기 함수 처리 이다.
이전 챕터에서 비동기 함수 처리를 위해 then , catch , finally 와 같은 비동기 함수를 사용했다.
이번에 실습할 비동기 함수 처리는 다음과 같다.
<script>
const url = 'https://jsonplaceholder.typicode.com/todos/1';
fetch(url)
.then((res) => res.json())
.then(console.log)
.catch(console.error);
</script>

이것을 제네레이터를 이용해서 사용해보자
<script>
const async = (generatorFunc) => {
const generator = generatorFunc();
const onResolved = (arg) => {
const result = generator.next(arg);
return result.done
? result.value
: result.value.then((res) => onResolved(res));
};
return onResolved;
};
const asyncFetch = async(function* fetchTodo() {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const response = yield fetch(url);
const todo = yield response.json();
console.log(todo);
});
asyncFetch();
</script>
으아악 코드가 더 어려워졌는디요 ?
우선 async 를 보자
const async = (generatorFunc) => {
const generator = generatorFunc();
const onResolved = (arg) => {
const result = generator.next(arg);
return result.done
? result.value
: result.value.then((res) => onResolved(res));
};
return onResolved;
};
async 는 제너레이터 함수를 인수로 받아 generator 변수에 담은 후 generator 변수를 기억하는 클로저 함수인 onResolved 라는 함수를 반환한다.
onResolved 함수는 generator 의 next() 값을 반환하는데 만약 반환값이 done : false 라면 재귀적으로 done : true 가 될 때 까지 반복한다.
return result.done ? result.value : result.value.then((res) => onResolved(res)) 를 보면 then 을 이용하기 때문에 res 값이 프로미스 객체 일 경우 비동기 처리가 완료된 후 재귀함수가 시작된다.
const asyncFetch = async(function* fetchTodo() {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const response = yield fetch(url);
const todo = yield response.json();
console.log(todo);
});
asyncFetch();
결국 이 부분을 보면 fetchTodo 라는 제너레이터 함수를 이용해 asyncFetch 라는 객체를 만든다.
asyncFetch 는 next 가 호출 될 때 마다 fetch(url) 을 반환하여 첫 프로미스 객체를 반환하고
그 다음 호출 때엔 promise 객체 body 프로퍼티에 존재하는 결과값을 json 메소드를 이용해 객체 타입으로 todo 라는 값에 저장 후 반환한다.

<script>
const async = (generatorFunc) => {
const generator = generatorFunc();
const onResolved = (arg) => {
const result = generator.next(arg);
return result.done
? result.value
: result.value.then((res) => onResolved(res));
};
return onResolved;
};
const asyncFetch = async(function* fetchTodo() {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const response = yield fetch(url);
const todo = yield response.json();
console.log(todo);
});
asyncFetch();
</script>
결국 전체 코드를 보면 fetch(url) 부터 시작해서 생기는 비동기 처리들을 마치 동기 처리 되는 함수들의 형식과 비슷하게 나타낼 수 있었다.
비동기 처리되고 결과값에 따라
then을 이용하기 때문에 비동기 처리들을 사용한다는 점은 동일하다 .
<script>
const url = 'https://jsonplaceholder.typicode.com/todos/1';
fetch(url)
.then((res) => res.json())
.then(console.log)
.catch(console.error);
</script>
프로미스 체이닝을 썼던 then , catch , finally 는 일반적인 문법과 달랐기 때문에 어쩌면 프로미스 체이닝보다 위의 제너레이터가 우리에겐 익숙 할 수 있다.
솔직히 난 더 어려워보인다.
then , catch , finally가 더 직관적인거 같은디 .. 장점이 뭘까 ?
async/await위에선 async / await 를 간략하게 구현했지만 코드가 무척이고 장황하고 가독성도 나빠졌다.
ㅇㅈ
ES8(ECAMScript 2017) 에서는 제너레이터보다 간단하고 가독성이 좋게 비동기 처리를 동기 처리처럼 동작 하게 구현 할 수 있는 async/await 가 도입되었다.
async/await 는 프로미스 객체를 기반으로 동작한다. async/await 는 프로미스의 then , catch , finally 와 같이 프로미스 체이닝을 이용하지 않고 마치 동기 처리처럼 프로미스가 처리 결과를 반환 할 수록 구현 할 수 있다.
<script>
async function fetchTodo() {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const response = await fetch(url);
const todo = await response.json();
console.log(todo);
// {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
}
fetchTodo();
</script>
와우!!!!!!!!!!!!!!!!!!!!!!! 굿
asyncasync 키워드는 언제나 Promise 객체를 반환한다.
만약 async 키워드 후 선언된 함수들이 Promise 객체를 생성하지 않는 것들이라 하더라도 무조건 Promise 객체로 생성하고 반환값을 resolve 하는 프로미스를 반환한다.
<script>
const foo = async () => {
return 1;
};
console.log(foo());
</script>

async는 함수 선언문, 표현식, 화살표 함수, 메소드에서 사용 가능하지만클래스내부constructor메소드에선 사용이 불가하다.
class Example {
async constructor() {}
}
const ex = new Example()
// Uncaught SyntaxError: Class constructor may not be an async method
그 이유는 class 의 async 는 항상 어떤 객체를 반환해야 하는데 async 는 Promise 객체를 반환한다.
이는 class 의 원 기능과 맞지 않는다. class 는 constructor 내부에서 정의된 인스턴스를 객체 타입 형태로 전달해야 한다.
awaitawait 키워드는 프로미스가 settled 된 상태까지 대기하다가 settled 상태가 되면 resloved 한 처리 결과를 반환한다.
awiat 키워드는 반드시 Promise 객체 앞에 사용해야 하며, async 함수 내부에서 사용해야 한다.
<script>
const fetchTodo = async (id) => {
const url = `https://jsonplaceholder.typicode.com/todos/${id}`;
const response = await fetch(url);
const json = await response.json();
console.log(json)
return json;
};
const result = fetchTodo(1);
console.log(result)
</script>
다음과 같은 코드가 있을 때 json 과 result 는 어떤 모습으로 로그 될 것 같은가 ?
나는
json은{}태그에 담겨 결과값이return되고result도 같은 모습으로return될 것이라고 생각했다.
결과는
<script>
...
console.log(json)
// {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
};
...
console.log(result)
// Promise {<fulfiled> ..}
</script>
json 은 json() 화 시킨 객체 그대로 잘 로그되는데 반환값은 Promise 객체이다. 나는 그대로 json 을 바로 반환했는데 말이다.

그건 위에서 설명한 부분으로 설명 가능하다.
async 키워드가 존재하는 함수의 반환값은 항상 Promise 객체였다.
그러니 반환된 json 은 사실 반환 될 때 Promise.resolve(json) 으로 감싸져서 반환되는 것과 같다.
그러니 const json = await response.json(); 에서 json 에 반환되는 값은 JSON 객체일지언정 반환되는 값은 Promise.resolve(json) 인 것이다.
비동기 함수의 반환값을 다른 변수에 동기적으로 설정하는 것은 역시 힘들다.
그럼 Promise 객체로 반환된 값을 꺼내 다루기 위해선
const fetchTodo = async (id) => {
const url = `https://jsonplaceholder.typicode.com/todos/${id}`;
const response = await fetch(url);
const json = await response.json();
// 여기서 json 객체를 가지고 할 수 있는 일을 하든지
};
해당 비동기 처리로 감싸진 async 코드 블록 내에서 하든지
const logJson = async (id) => {
const result = await fetchTodo(id);
console.log(result);
};
logJson(1); // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
다른 비동기 함수를 이용해 await 로 받아온 후 이후 로직들을 실행하면 된다.
아~!니! 여기서도
async인데 얘는 왜Promise객체를 반환하지 않고JSON객체를 반환하냐고 ~!fetchTodo는 이미Promise객체를 반환했는데 !!!
awiat는 반환받은Promise객체가resolved한 값을 뺴오기 때문에await fetchTodo()는Promise객체 안에서resolved된 값을 꺼내온다.결국
Promise.prototype.then과 유사한 느낌이다.
한 번 더 개념을 머리속에 정리하고 가자
await 는 비동기 처리가 완료 될 때 까지 (선언된 Promise 객체가 settled 될 때 까지) 코드 블록을 중지해뒀다가 settled 되면 다음 코드 블록으로 넘어간다.
그렇기 때문에 비동기 처리 함수의 결과값을 이용해서 또 다른 비동기 처리 or 동기 처리를 해야 할 경우 유용하게 사용된다 .
<script>
const foo = async () => {
const a = await new Promise((res) => setTimeout(() => res(1), 1000));
const b = await new Promise((res) => setTimeout(() => res(a + 1), 2000));
const c = await new Promise((res) => setTimeout(() => res(b + 2), 3000));
console.log(c); // 4
};
foo();
</script>
이를 마냥 then , catch 문을 이용했다면 다음처럼 작성해야 한다.
<script>
const foo = () => {
new Promise((res) => setTimeout(() => res(1), 1000))
.then((a) => setTimeout((a) => a + 1), 2000)
.then((b) => setTimeout((b) => b + 2), 3000)
.then(console.log); // 4
};
foo();
</script>
async/await 의 에러처리이전 비동기 처리의 에러처리는 어렵다고 했었다.
try {
setTimeout(() => {
throw new Error('error!');
}, 1000);
} catch (err) {
() => console.error(err);
}
// Uncaught Error: error!
// 에러를 잡지 못하고 에러가 발생함
에러는 호출자 방향으로 전파되는데 try 문에서 호출된 비동기 함수인 setTimeout 은 호출 후 try 블록을 빠져 나가고 , setTImeout 내부에 존재하는 throw new Error 는 try 문이 아닌 이벤트 루프에서 호출되기 때문에 에러의 방향이 이벤트 루프를 향한다.
그렇기 때문에 try 문으로 향하는 에러를 catch 하러는 catch(err) 는 에러를 잡지 못한다.
하지만 async/await 를 사용하면 비동기 함수를 호출하는 존재가 try 블록 내부에 존재하기 때문에 에러를 캐치 할 수 있다.
<script>
const asyncError = async () => {
try {
await new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('error!'));
}, 1000);
});
} catch (err) {
console.log(err); // Error: error!
}
};
asyncError();
</script>
async / await 가 then , catch , finally 보다 추천되는 이유async/await 를 사용하면 비동기 코드를 우리에게 익숙한 동기 코드 처럼 작성 할 수 있다. 이로인해 가독성이 올라간다.
then / catch / finally 패턴은 콜백 함수의 중첩이 발생 할 수 있고 코드를 이해하기 어렵게 할 수 있다.
에러 처리
async/await 는 try/catch 블록을 사용 할 수 있어 에러를 처리하는 것이 가능하다.
각
then절 마다 에러를 캐치하고 싶다면then.catch.then.catch.then.catch처럼 사용해야 하는데 벌써부터 어지럽다.
변수 범위 및 값 전달
async/await 를 사용하면 비동기 처리의 결과값을 스코프 내에서 할당하고 처리하는 것이 가능하였다.
const foo = async () => {
const a = await new Promise((res) => setTimeout(() => res(1),
1000)); // 비동기 처리의 결과값을 foo 함수의 렉시컬 스코프에서 할당 가능
...
이는 변수들의 범위를 제한함으로서 더욱 안전하게 변수를 사용 할 수 있다.
디버깅
async/await 를 사용하면 스택 트레이스가 더 명확히 나타나기 때문에 비동기 작업의 위치를 추적하기 쉽다.
then 을 사용하면 then 블록 내부에서 발생한 오류를 추적하기 어려울 수 있다.
조합성과 유연성
async/await 는 기존의 동기 코드와 쉽게 통합되며, 기존 코드를 수정하지 않고도 비동기 코드를 추가 할 수 있다.
then , catch로 프로미스 체이닝을 하다가 새로운 비동기 작업을 추가하려고 하면 체이닝 사이에then, catch를 넣어줘야 한다.
종합적으로 async/await 는 기존의 동기 코드와 쉽게 통합되며, 기존 코드를 수정하지 않고도 비동기 코드를 추가 할 수 있다.
또한 비동기처리를 위한 콜백 패턴을을 사용하지 않고, 동기 처리 양식으로 사용해야 할 수 있기 때문에 추천된다고 한다.
회고
사람들이 자주 쓴느
async/await가 대체 뭘까 궁금했는데 이번에 좀 알 수 있었다.
확실한 것은 공용 API 를 가져오는토이프로젝트를 하면서then , catch를 사용 할 떄와async/await를 사용 할 때의 장단점을 더 체감 할 수 있도록 해야겠다.