제너레이터는 코드 블록의 실행을 일시 중지 했다가 필요한 시점에 재개 할 수 있는 특수한 함수이다.
생각해보면 일반적으로 함수가 호출 된 순간 함수의 동작은 우리가 제어 할 수 없다.
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>
와우!!!!!!!!!!!!!!!!!!!!!!! 굿
async
async
키워드는 언제나 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
내부에서 정의된 인스턴스를 객체 타입 형태로 전달해야 한다.
await
await
키워드는 프로미스가 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
를 사용 할 때의 장단점을 더 체감 할 수 있도록 해야겠다.