제너레이터와 async/await
[Javascript] 비동기, Promise, async, await 확실하게 이해하기
제너레이터란?
- 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수 함수
제너레이터와 일반 함수의 차이
- 제너레이터 함수는 함수 호출자에게 함수 실행의 제어권을 양도할 수 있다.
- 제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다.
- 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
제너레이터 함수의 정의
function*
키워드로 선언- 하나 이상의
yield
표현식을 포함
// 제너레이터 함수 선언문
function* genDecFunc() {
yield 1;
}
//제너레이터 함수 표현식
const genExpFunc = function* () {
yield 1;
};
//제너레이터 메서드
const obj = {
* genObjMethod() {
yield 1;
}
}
// 제너레이터 클래스 메서드
class MyClass {
* genClsMethod() {
yield 1;
}
}
*
의 위치는function
키워드와 함수 이름 사이라면 상관 없음
function* genFunc() { yield 1; } => 권장
function * genFunc() { yield 1; }
function *genFunc() { yield 1; }
function*genFunc() { yield 1; }
- 화살표 함수로 정의 불가
const genArrowFunc = * () => {
yield 1;
};
new
연산자와 함께 생성자 함수로 호출 불가
function* genFunc() {
yield 1;
}
new genFunc(); // TypeError
제너레이터 객체
- 제너레이터 객체를 생성해 변환
제너레이터 객체는 이터러블이면서 이터레이터
// 제너레이터 함수
function* genFunc() {
yield 1;
yield 2;
yield 3;
}
// 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
const generator = genFunc();
// 제너레이터 객체는 이터러블이면서 동시에 이터레이터
// 이터러블은 Symbol.iterator 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속받은 객체
console.log(Symbol.iterator in generator); // true
// 이터레이터는 next 메서드를 갖는다.
console.log('next' in generator); // true
제너레이터 객체가 갖는 메서드
next
,return
,throw
next
- 제너레이터 함수의
yield
표현식까지 코드 블록을 실행하고yield
된 값을value
프로퍼티 값으로,false
를done
프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환
return
- 인수로 전달받은 값을
value
프로퍼티 값으로,true
를done
프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환
throw
- 인수로 전달받은 에러를 발생시키고
undefined
를value
프로퍼티 값으로,true
를done
프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환
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* genFunc() {
yield 1;
yield 2;
yield 3;
}
const generator = genFunc();
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
표현식까지 실행되고 중지 next
메서드는 이터레이터 리절트 객체를 반환value
프로퍼티에yield
된 값 1이 할당- 아직 함수가 끝까지 실행되지 않았으므로
done
의 값에는false
가 할당
- 첫 번째
- 두 번째
next
메서드 호출- 두 번째
yield
표현식까지 실행되고 중지 next
메서드는 이터레이터 리절트 객체를 반환value
프로퍼티에yield
된 값 2이 할당- 아직 함수가 끝까지 실행되지 않았으므로
done
의 값에는false
가 할당
- 두 번째
- 세 번째
next
메서드 호출- 세 번째
yield
표현식까지 실행되고 중지 next
메서드는 이터레이터 리절트 객체를 반환value
프로퍼티에yield
된 값 3이 할당- 아직 함수가 끝까지 실행되지 않았으므로
done
의 값에는false
가 할당
- 세 번째
- 네 번째
next
메서드 호출- 남은
yield
표현식이 없으므로 제너레이터 함수의 마지막까지 실행 next
메서드는 이터레이터 리절트 객체를 반환value
프로퍼티에undefined
이 할당- 함수가 끝까지 실행되었으므로
done
의 값에는true
가 할당
- 남은
제너레이터에 인수 전달
- 제너레이터 객체의
next
메서드에 전달한 인수는 제너레이터 함수의yield
표현식을 할당받는 변수에 할당 ⇒ 제너레이터 객체의next
메서드에 인수 전달 가능
function* genFunc() {
const x = yield 1;
const y = yield (x + 10);
return x + y;
}
const generator = genFunc(0);
let res = generator.next();
console.log(res); // {value: 1, done: false}
res = generator.next(10);
console.log(res); // {value: 20, done: false)
res = generator.next(20);
console.log(res); // {value: 30, done: true}
- 첫 번째
next
메서드 호출- 처음 호출하는
next
메서드에는 인수를 전달하지 않는다. ⇒ 전달되면 무시 yield 1
표현식까지 실행되고 일시중지value
프로퍼티에는yield
된 값 1이 전달- 함수가 아직 끝나지 않았으므로
done
값은false
- 처음 호출하는
- 두 번째
next
메서드 호출next
메서드에 인수로 전달한 10은genFunc
함수의 x 변수에 할당 ⇒const x = yield 1
은 두 번째next
메서드를 호출했을 때 완료yield (x + 10)
표현식까지 실행되고 일시중지value
프로퍼티에는yield
된 값 20이 전달- 아직 함수가 끝까지 실행되지 않았으므로
done
의 값에는false
가 할당
- 세 번째
next
메서드 호출next
메서드에 인수로 전달한 20은genFunc
함수의 y 변수에 할당
⇒const y = yield (x + 10)
은 두 번째next
메서드를 호출했을 때 완료yield (x + 10)
표현식까지 실행되고 일시중지- 반환값
x + y
는value
프로퍼티에 할당 - 제너레이터는 값을 반환할 필요가 없고
return
은 종료의 의미로만 사용 value
프로퍼티에는yield
된 값 30이 전달- 함수가 끝까지 실행되었으므로
done
의 값에는true
가 할당
제너레이터의 활용
이터러블의 구현
- 제너레이터 함수를 사용하여 보다 간단한 이터러블을 구현할 수 있다.
//무한 이터러블을 생성하는 함수
const infiniteFibonacci = (function () {
let [pre, cur] = [0, 1];
return {
[Symbol.iterator]() { return this; },
next() {
[pre, cur] = [cur, pre + cur];
// 무한 이터러블이므로 done 프로퍼티 생량
return { value: cur };
}
}
}());
for (const num of infiniteF) {
if (num > 1000) break;
console.log(num); // 1 2 3 5 8 ... 2584 4181 6765
}
//제너레이터 함수를 이용한 이터러블
const infitFibo = (function* () {
let [pre, cur] = [0, 1];
while (true) {
[pre, cur] = [cur, cur + pre];
yield cur; // value 프로퍼티 값으로 cur 할당
}
}());
for (const num of infitFibo){
if(num>1000) break;
console.log(num)
}
비동기 처리
- 프로미스의 후속 처리 메서드
then
,catch
,finally
없이 비동기 처리 결과를 반환하도록 구현할 수 있다.
const { response } = require('express');
const fetch = require('node-fetch');
const async = generatorFunc => {
const generator = generatorFunc(); // 2
const onResolved = arg => {
const result = generator.next(arg); // 5
return result.done
?result.value :
result.value.then(res => onResolved(res)); // 7
}
return onResolved; // 3
}
(async(function * fetchTodo(){ // 1
const url = 'https://jsonplaceholder.typicode.com/post/1';
const res = yield fetch(url); // 6
const todo = yield response.json(); // 8
console.log(todo);
})()) // 4
- 1
async
함수가 호출되면(1) 인수로 전달받은 제러네이터 함수fetchTodo
를 호출해서 제너레이터 객체를 생성(2)하고onResolved
함수를 반환(3)한다.onResolved
함수는 상위 스코프의generator
변수를 기억하는 클로저이다.async
함수가 반환한onResolved
함수를 즉시 호출(4)해 (2)에서 생성한 제너레이터 객체의next
메서드를 처음 호출(5)한다- 2
next
메서드가 처음 호출(5)되면 제너레이터 함수fetchTodo
의 첫번째yield
문(6)이 실행된다. 이때next
메서드가 반환한 이터레이터 리절트 객체의done
프로퍼티 값이false
, 즉 아직 함수가 끝까지 실행되지 않았다면 이터레이터 리절트 객체의value
프로퍼티 값, 즉 첫번째yield
된fetch
함수가 반환한 프로미스가resolve
한Response
객체를onResolved
함수에 전달하면서 재귀호출(7)한다- 3
onResolved
함수에 인수로 전달된Response
객체를next
메서드에 인수로 전달하면서next
메서드를 두번째 호출(5)한다. 이때next
메서드에 인수로 전달한Response
객체는 제너레이터 함수fetchTodo
의response
변수(6)에 할당되고 제너레이터 함수fetchTodo
의 두번째yield
문(8)까지 실행된다.- 4
next
메서드가 반환한 이터레이터 리절트 객체의done
프로퍼티 값이false
. 즉 아직 제너레이터 함수fetchTodo
가 끝까지 실행되지 않았다면 이터레이터 리절트 객체의value
프로퍼티 값, 즉 두번째yield
된response.json
메서드가 반환한 프로미스가resolve
한todo
객체를onResolved
함수에 인수로 전달하면서 재귀호출(7)한다.- 5
onResolved
함수에 인수로 전달된todo
객체를next
메서드에 인수로 전달하면서next
메서드를 세번째로 호출(5)한다. 이때next
메서드에 인수로 전달한todo
객체는 제너레이터 함수fetchTodo
의todo
변수(8)에 할당되고 제너레이터 함수fetchTodo
가 끝까지 실행된다.- 6
next
메서드가 반환된 이터레이터 리절트 객체의done
프로퍼티 값이true
, 즉 제너레이터 함수fetchTodo
가 끝까지 실행되었다면 이터레이터 리절트 객체의value
프로퍼티 값, 즉 제너레이터 함수fetchTodo
의 반환값인undefined
를 그대로 반환(9)하고 처리를 종료한다.
46.6 async / await
- 제너레이터보다 간단하고 가독성 좋게 비동기 처리를 동기처럼 동작하도록 구현하기 위하여 만들어짐
- 프로미스를 기반으로 동작
- 프로미스의
then/catch/finally
후속 처리 메서드에 콜백 함수를 전달해서 비동기 처리 결과를 후속 처리할 필요 없이 마치 동기 처리처럼 프로미스를 사용 가능
async
async
키워드를 function 앞에 사용해 정의하며 언제나 프로미스 반환- 프로미스를 반환하는 비동기적인 함수를 만들어주는 키워드
- 프로미스가 아닌 값을 반환하더라도 resolved promise 로 값을 감싸 프로미스 형태로 반환
async function f() {
return 1;
}
f().then(alert); // 1
- 함수 호출 시
result
가1
인 이행 프로미스가 반환
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
- 명시적으로 프로미스 반환
// 함수 선언문
async function foo(n) {return n;}
foo(1).then(v => console.log(v)) //1
// async 함수 표현식
const bar = async function(n) {return n};
bar(2).then(v => console.log(v)); //2
// async 화살표 함수
const baz = async n => n;
baz(3).then(v=>console.log(v)) //3
// async 메서드
const obj = {
async foo(n) {return n;}
};
obj.foo(4).then(v => console.log(v));
// async 클래스 메서드
class MyClass {
async bar(n) {return n;}
}
const myClass = new MyClass();
myClass.bar(5).then(v => console.log(v)) //5
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const getDog = async () => {
await sleep(1000);
return '댕댕';
}
async function process() {
const dog = await getDog();
console.log(typeof getDog());
console.log(typeof dog);
console.log(dog);
}
process();
/*
object
string
댕댕
*/
async
함수는 반환하는 값은 Promise 객체 ⇒object
await
함수가 반환하는 값은 원래의 값의 형태 ⇒string
await
- 프로미스가
settled
상태(비동기 처리가 수행된 상태)가 될 때까지 대기하다가settled
상태가 되면 프로미스가resolve
한 처리 결과를 반환 - 반드시
async
함수 내부에서 사용해야 함 promise.then
보다 더 깔끔하게 프로미스의result
값을 얻을 수 있도록 하는 문법
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)
console.log(result); // "완료!"
}
f();
- 함수를 호출하고, 본문이 실행되는 도중에 (*) 표시 줄에서 실행이 중단되었다가 프로미스가 처리되면 실행이 재개
- 이때 프로미스 객체의
result
값이 변수result
에 할당
const getGitUserName = async id => {
const res = await fetch(`https://api.github.com/users/${id}`); ✔️ (1)
const name = await res.json(); ✔️ (2)
console.log(name);
};
getGitUserName('ungmo2');
- (1)의 fetch 함수가 수행한 HTTP 요청에 대한 서버의 응답이 도착해서 fetch 함수가 반환한 프로미스가 settled 상태가 될 때까지 (1)은 대기
- 이후 ****프로미스가 settled 상태가 되면 프로미스가 resolve한 처리 결과가
res
변수에 할당된다.
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));
console.log([a,b,c]); //[ 1, 2, 3 ]
}
foo(); // 약 6초 소요된다
- 프로미스 각각 3, 2, 1초 후 settled 상태가 되므로 총 소요시간은 3 + 2 + 1 = 6초이다.
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))
])
console.log(res); //[ 1, 2, 3 ]
}
foo(); // 약 3초 소요된다
- 비동기적 처리를 하여 개별적으로 처리하므로 1, 2, 3초 중 가장 오래 걸리는 3초가 총 소요시간이 된다.
async function bar() {
const a = await new Promise(resolve => setTimeout(()=>resolve(n), 3000));
const b = await new Promise(resolve => setTimeout(()=>resolve(a+1), 2000));
const c = await new Promise(resolve => setTimeout(()=>resolve(b+1), 1000))
console.log([a,b,c]); //[ 1, 2, 3 ]
}
bar(1); // 약 6초 소요된다
- 다음의 bar 함수는 앞선 비동기 처리의 결과를 가지고 다음 비동기 처리를 수행해야 한다.
- 따라서 비동기 처리의 처리 순서가 보장되어야 하므로 모든 프로미스에
await
키워드를 써서 순차적으로 처리할 수 밖에 없다.
46.6.3 에러 처리
- 에러는 호출자 방향으로 전파 ⇒ 콜 스택의 아래 방향(실행 중인 실행 컨텍스트가 푸시되기 직전에 푸시된 실행 컨텍스트 방향)으로 전파
- 하지만 비동기 함수의 콜백 함수를 호출한 것은 비동기 함수가 아니기 때문에
try...catch 문
을 사용해 에러 캐치 불가
try{
setTimeout(()=>{ throw new Error('Error!');}, 1000);
} catch(e){
console.error(e);
}
- async/await 에서의 에러 처리는
try...cath 문
사용 가능⇒ 콜백 함수를 인수로 전달받는 비동기 함수와는 달리 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문 - why?
const fetch = require('node-fetch');
const foo = async () => {
try{
const wrongUrl = 'https://잘못된url';
const response = await fetch(wrongUrl);
const data = await response.json();
console.log(data);
} catch(err){
console.error(err); // TypeError
}
};
foo();
- async 함수 내에서 catch문을 사용해서 에러 처리를 하지 않으면 async 함수는 발생한 에러를 reject하는 프로미스를 반환한다.
Promise.prototype.catch
후속 처리 메서드 사용
const fetch = require('node-fetch');
const foo = async() => {
const wrongUrl = 'https://잘못된url';
const response = await fetch(wrongUrl);
const data = await response.json();
return data;
};
foo()
.then(console.log)
.catch(console.error) // TyepError : Failed to fetch
비동기 함수와 Promise와 async/await
//비동기 함수
timer(1000, function() {
console.log('작업1');
timer(1000, function() {
console.log('작업2');
timer(1000, function() {
console.log('작업3');
});
});
});
//작업1 -> 작업2 -> 작업3 (1초 간격으로)
- 비동기 함수 사용 시 콜백 함수를 이용하여 비동기적으로 처리
//Promise 활용
timer(1000)
.then(function() {
console.log('작업');
return timer(1000);
})
.then(function() {
console.log('작업');
return timer(1000);
}
.then(function() {
console.log('작업');
}
- 프로미스를 리턴하면 리턴 값인 프로미스의
then
을 이용하여 연속적인 비동기적으로 작업 가능
//async,await
async function run() {
await timer(1000)
console.log('작업');
await timer(1000)
console.log('작업');
await timer(1000)
console.log('작업');
}
run();
await
키워드를 이용하여 동기적인 것처럼 보이는 비동기적 작업을 수행
async/await 이용하기
console.log('start');
timer(1000).then(function(time) {
console.log('time:'+time);
return timer(time+1000);
}).then(function(time) {
console.log('time:'+time);
return timer(time+1000);
}).then(function(time) {
console.log('time:'+time);
console.log('end');
});
async function run() {
console.log('start');
var time = await timer(1000); // 위 코드의 첫 번째 then 내부의 첫 번째 파라미터를 반환
console.log('time:'+time); // settled 상태
time = await timer(time+1000);
console.log('time:'+time); // settled 상태
time = await timer(time+1000);
console.log('time:'+time); // settled 상태
console.log('end');
}
run();
/*
start
time:1000
time:2000
time:3000
end
*/
Promise.all
- 여러 비동기 동작을 한꺼번에 기다림
// 0 ~ 9번 직원의 나이 평균을 구하는 함수
function setTimeoutPromise(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), ms);
});
}
async function fetchAge(id) {
await setTimeoutPromise(1000);
console.log(`${id} 사원 데이터 받아오기 완료!`);
return parseInt(Math.random() * 20, 10) + 25;
}
async function startAsyncJobs() {
let ages = [];
for (let i = 0; i < 10; i++) {
let age = await fetchAge(i);
ages.push(age);
}
console.log(
`평균 나이는? ==> ${
ages.reduce((prev, current) => prev + current, 0) / ages.length
}`
);
}
startAsyncJobs();
//출력값
0 사원 데이터 받아오기 완료!
1 사원 데이터 받아오기 완료!
2 사원 데이터 받아오기 완료!
3 사원 데이터 받아오기 완료!
4 사원 데이터 받아오기 완료!
5 사원 데이터 받아오기 완료!
6 사원 데이터 받아오기 완료!
7 사원 데이터 받아오기 완료!
8 사원 데이터 받아오기 완료!
9 사원 데이터 받아오기 완료!
평균 나이는? ==> 33
- 총 10초 걸림
- 비동기라면 1초 걸려야 하는데 10초 걸린 이유는?
⇒ let age = await fetchAge(i);
async function startAsyncJobs() {
let promises = [];
for (let i = 0; i < 10; i++) {
promises.push(fetchAge(i));
}
let ages = await Promise.all(promises);
console.log(
`평균 나이는? ==> ${
ages.reduce((prev, current) => prev + current, 0) / ages.length
}`
);
}
startAsyncJobs();
for
문 안에서await
를 하는 대신 Promise 를 배열에 쌓는다.Promise.all
함수는 인자로 Promise 배열을 받으며 하나의 특별한 Promise를 새로 생성- 모든 비동기 작업이 성공했다면
resolve
호출, 하나라도 실패했다면reject
호출
//출력값
0 사원 데이터 받아오기 완료!
1 사원 데이터 받아오기 완료!
2 사원 데이터 받아오기 완료!
3 사원 데이터 받아오기 완료!
4 사원 데이터 받아오기 완료!
5 사원 데이터 받아오기 완료!
6 사원 데이터 받아오기 완료!
7 사원 데이터 받아오기 완료!
8 사원 데이터 받아오기 완료!
9 사원 데이터 받아오기 완료!
평균 나이는? ==> 33
- 총 1초 걸림
async/await 병렬 처리
async function getApple() {
await delay(1000);
return '사과';
}
async function getBanana() {
await delay(1000);
return '바나나';
}
async function pickFruites() {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
}
pickFruites().then(console.log); // 사과 + 바나나
- 총 2초 소요
- 하지만
getApple()
메소드와getBanana()
메소드는 독립적이기 때문에 순차적으로 진행할 필요가 없다.
async function pickFruites() {
const applePromise = getApple();
const bananaPromise = getBanana();
const apple = await applePromise;
const banana = await bananaPromise;
return `${apple} + ${banana}`;
}
pickFruites().then(console.log); // 사과 + 바나나
- 총 1초 소요
- 메소드 별 반환 값을 할당하는 변수를 생성하여 그 변수를 통해
await
를 사용하면 두 메소드를 비동기적으로 처리가 가능하여 1초 소요된다.
프로미스를 async/await 로 바꾸기
// 프로미스 사용
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new Error(response.status);
}
})
}
loadJson('no-such-user.json')
.catch(alert); // Error: 404
// async&await 사용
async function loadJson(url) { // (1)
let response = await fetch(url); // (2)
if (response.status == 200) {
let json = await response.json(); // (3)
return json;
}
throw new Error(response.status);
}
loadJson('no-such-user.json')
.catch(alert); // Error: 404 (4)
1) 함수 loadJson
에 async
를 붙여준다.
2) Promise 를 리턴하는 모든 비동기 함수 호출부 앞에 await
키워드가 추가
3) await
를 사용해도 되지만, return response.json()
를 사용해도 된다.
4) loadJson
에서 던져진 에러는 .catch
에서 처리된다.
'Frontend > JavaScript' 카테고리의 다른 글
[JavaScript] 클릭한 태그 찾기 (0) | 2023.02.23 |
---|---|
[JavaScript] javascript 주요 개념 간단 정리 (0) | 2023.02.22 |
[JavaScript] DOM 이벤트 (0) | 2023.02.16 |
[JavaScript] Set (2) | 2023.02.16 |
[JavaScript] 이터러블 (1) | 2023.02.16 |