비동기 활동을 모두 완료하고 진행해야할 때 우리는 Promise.All
을 사용해야한다.
filter에서의 실수
// delay에 따라서 sec 값을 비동기적으로 반환해주는 함수
const afterTime = (sec) => {
console.log("afterTime>>", sec);
if (sec < 1 || sec > 3)
return Promise.reject(new Error("Not valid sec range!!"));
return new Promise((resolve) => setTimeout(resolve, sec * 1000, sec));
};
const odds = [1, 2, 3].filter(async val => {
const r = await afterTime(val);
console.log(r);
return r % 2 === 1;
});
console.log('odds=', odds);
// [1, 2, 3]
// 원하는 결과 값 [1, 3] // 홀수 값
arr.filter 메서드의 관심은 true와 false 뿐이다.
이 정신나간녀석은 결괏값이 출력되기도 전에 filter의 return 값을 출력했다. 왜?
filter에 async를 건 순간부터 예정되있던 일이다. async는 해당 값을 프로미스로 리턴하기 때문에 배열에는 모두 truesy
한 값이 담기 때문이다.
[1, 2, 3] = [프로미스, 프로미스, 프로미스] = [truesy, truesy, truesy]
여기서 프로미스all을 써보면 어떨까
const res = [1, 2, 3].map(afterTime);
const resres = await Promise.all(res);
const resresres = resres.filter((a) => a % 2 === 1);
console.log("🚀res", res);
console.log("🚀 resres", resres);
console.log("🚀 resresres", resresres);
풀어서 쓰면 다음과 같은 방식으로 해결 할 수 있다.
map을 활용해 해당 프로미스들을 arr에 담아둔다. Promise.all을 활용해 그 값들을 fulfiled 한다. 그리고 filter를 돌려 활용한다.
더 간결하게 표현하자면 아래 코드로 같은 값을 얻을 수 있다.
console.log(
"odds=",
(await Promise.all([1, 2, 3].map(afterTime))).filter((a) => a % 2 === 1)
);
그 것은 지금 Promise.all의 위치가 전역 global 환경이기 때문이다.
근데 여기서 async/await 가 필요할까?
const odds = (
await Promise.all(
[1, 2, 3].map( async (val) => { 👈🏽
const r = await afterTime(val); 👈🏽
console.log(r);
return r;
})
)
).filter((res) => res % 2 === 1);
console.log("odds=", odds);
정답은 필요없다!
비동기처리는 Promise.all 이 해결해주기 때문에`로 없애는게 좋다.
const afterTime = (sec) => {
console.log("afterTime>>", sec);
if (sec < 1 || sec > 3)
return Promise.reject(new Error("Not valid sec range!!"));
return new Promise((resolve) => setTimeout(resolve, sec * 1000, sec + 4));
};👈👈👈
const odds = (
await Promise.all(
[1, 2, 3].filter((val) => { 👈
const r = afterTime(val);
console.log(r);
return r;
})
)
).filter((res) => res % 2 === 1);
console.log("odds=", odds);
차이점을 알아보기 위해 afterTime의 return 인자 sec에 + 3초 해줘보겠다.
array.filter를 활용했을 때!
매우 빠르다. 11.559ms 오오... 근데 우리가 원한 값이 아니다.
위에서 말한 truesy 한 값을 배출하기 때문에 r의 값의 비교없이 돌아간다.
array.map을 활용했을 때!
3초가 걸렸다. 결과값을 확인하고 그 결과 값을 반환하는 배열이 출력되었다.
다양한 TDD를 해봐야하고, 비동기에서는 filter를 쓰는 것을 심각하게 고려해봐야 할 것이다.
//다음 코드를 병렬로 실행하여 3.x초에 수행되도록 promiseAll 함수를 재작성(refactoring)하시오.
const afterTime = sec => {
console.log('afterTime>>', sec);
if (sec < 1 || sec > 3) return Promise.reject(new Error('Not valid sec range!!'));
return new Promise(resolve => setTimeout(resolve, sec * 1000, `${sec}초`));
};
const promiseAll = async promises => {
const results = [];
for (let i = 0; i < promises.length; i += 1) {
results[i] = await promises[i](i + 1);
}
return results;
};
console.time('async-promiseAll');
const pfns = [afterTime, afterTime, afterTime];
const rets = await promiseAll(pfns);
console.log('rets>>>', rets);
console.timeEnd('async-promiseAll');
async/await를 사용해 해당 함수가 비동기를 병렬적으로 수행하는 모습(낭비)을 보고 있다.
3초면 될 일을 6초를 소비했다. 이를 어쩌면 좋을까
for-await-of MDN
for await of 구문은 반복문 내부에서 실행되는 비동기 서비스들에 대한 순서를 보장해준다.
const promiseAll = async (promises) => {
const results = [];
const res = promises.map((item, idx) => item(idx + 1)); // (1)
for await (const k of res) results.push(k); // (2)
return results;
};
실행되지 않은 함수를 실행해서 promise로 반환된 객체를 보관해두는 곳이다.
promises를 map으로 돌려 idx 값을 활용해 리턴된 배열을 만든다.
for-await-of 구문을 활용해 병렬적으로 해당 promose들을 리턴해 results 배열에 담아준다.
results 배열을 반환한다. 비동기 함수들이 병렬적으로 실행되는 것을 알 수 있다.
무조건적으로 최신 문법을 사용해야한다고 생각했다. 무조건 적으로 async/await를 사용한다면 위와 같은 문제를 마주칠 것이다. 비동기에 대한 이해와 문법적 활용이 뒷받침되야 비로소 함수형프로그래밍의 비동기의 진짜 모습을 마주할 수 있을 것이다. (사실 아직도 잘 모르겠다^^) 비동기의 산을 반드시 넘는다!!
SSAC 영등포 교육기관에서 풀스택 실무 프로젝트 과정을 수강하고 있다. JS전반을 깊이 있게 배우고 실무에 사용되는 프로젝트를 다룬다. 앞으로 그 과정 중의 내용을 블로그에 다루고자 한다.