이 글은 원 글 작성자의 번역 허락을 받고 진행되었습니다. 의역, 오역 있을 수 있으니 댓글로 알려주신다면 감사드리겠습니다!
원글 보기 👉 https://zellwk.com/blog/async-await-in-loops/
시작하기 전에
이 글은 독자가 이미 async/await에 대해 기본적인 개념을 숙지하고 있다는 가정하에 진행되므로, async/await의 개념에 대해 숙지하고 글을 보는 것이 이해하는데 있어 도움이 될 것입니다.
예제 준비하기
먼저, 여러분이 과일바구니 안에 있는 과일의 수를 알고싶어한다고 가정해봅시다.
const fruitBasket = {
apple: 27,
grape: 0,
pear: 14
}
그리고 과일 바구니 안에 있는 각 과일의 수를 알고싶다면, getNumFruit
이라는 함수를 사용하도록 합니다.
const getNumFruit = fruit => {
return fruitBasket[fruit]
}
const numApples = getNumFruit('apple')
console.log(numApples) // 27
자, 이제는 fruitBasket
이 원격 저장소 어딘가에 저장되어 있고, 이에 접근하기 위해서는 1초가 걸린다고 가정해봅시다.
이 지연시간 1초는 아래와 같이 timeout을 사용해서 구현할 수 있습니다.
const sleep = ms => {
return new Promise(resolve => setTimeout(resolve, ms))
}
const getNumFruit = fruit => {
return sleep(1000).then(v => fruitBasket[fruit])
}
getNumFruit('apple')
.then(num => console.log(num)) // 27
마지막으로, 여러분은 비동기 함수를 이용해서 각 과일의 수를 얻기 위해 await getNumFruit
을 한다고 해봅시다.
const control = async _ => {
console.log('Start')
const numApples = await getNumFruit('apple')
console.log(numApples)
const numGrapes = await getNumFruit('grape')
console.log(numGrapes)
const numPears = await getNumFruit('pear')
console.log(numPears)
console.log('End')
}
아직 반복문은 사용하지 않았지만, 이렇게 해서 'await'을 반복해서 사용하는 모습을 볼 수 있습니다.
반복문안의 await
fruitBasket안에서 꺼내고 싶은 과일 배열을 아래와 같이 가지고 있다고 해봅시다.
const fruitsToGet = ['apple', 'grape', 'pear']
우리는 이 배열을 반복문을 통해서 접근할 수 있습니다.
const forLoop = async _ => {
console.log('Start')
for (let index = 0; index < fruitsToGet.length; index++) {
// Get num of each fruit
}
console.log('End')
}
반복문 안에서, getNumFruit
을 이용해서 각 과일의 수를 얻고 콘솔창에도 출력해봅시다.
getNumFruit
은 프로미스를 반환하기 때문에 await
을 이용해서 resolved 된 값을 기다릴 수 있습니다.
const forLoop = async _ => {
console.log('Start')
for (let index = 0; index < fruitsToGet.length; index++) {
const fruit = fruitsToGet[index]
const numFruit = await getNumFruit(fruit)
console.log(numFruit)
}
console.log('End')
}
await
을 사용하면, 여러분은 자바스크립트가 await
중인 프라미스가 resolved될 때까지 실행을 멈추는 것을 기대합니다. 즉, 반복문 안에서 await
은 순서대로 진행된다는 것을 의미합니다. 아래의 결과가 예상하는 결과일 것입니다.
'Start'
'Apple: 27'
'Grape: 0'
'Pear: 14'
'End'
이렇게 예상대로 동작하는 모습은 while
, for-of
와 같은 대부분의 반복문에서 볼 수 있습니다.
하지만, 예를들어 forEach
, map
, filter
, reduce
처럼 callback을 요구하는 반복문에서는 예상대로 동작하지 않습니다.forEach
, map
, filter
에서 await
이 어떻게 동작하는지는 곧 알아보겠습니다!
forEach 반복문안의 await
위의 for 반복문에서 진행해본 것 처럼 같은 예제를 사용하도록 하겠습니다.
먼저, 과일 배열을 반복문을 사용해서 접근해봅시다.
const forEachLoop = _ => {
console.log('Start')
fruitsToGet.forEach(fruit => {
// Send a promise for each fruit
})
console.log('End')
}
다음은, getNumFruit
으로 과일의 수를 구해보도록 하겠습니다.(callback함수 안의 aysnc
키워드를 주의하세요. 우리는 callback함수 안에서 await
을 써야하기때문에 async
키워드가 필요한 것입니다.)
const forEachLoop = _ => {
console.log('Start')
fruitsToGet.forEach(async fruit => {
const numFruit = await getNumFruit(fruit)
console.log(numFruit)
})
console.log('End')
}
아마 여러분은 아래와 같은 결과를 기대했을 것입니다.
'Start'
'27'
'0'
'14'
'End'
하지만 실제 결과는 다릅니다. 자바스크립트는 forEach 반복문 안의 promise가 resolve되기 전에 console.log('End')
를 실행시킵니다. 따라서 콘솔창에 실제로 찍힌 순서는 아래와 같습니다.
'Start'
'End'
'27'
'0'
'14'
왜냐하면 자바스크립트의 forEach는 promise를 인지하지 못하기 때문입니다. 그 말은, async/await을 forEach안에서 쓸 수없다는 뜻입니다.
map 안의 await
만약 map안에서 await을 쓴다면, map은 언제나 promise 배열을 반환합니다. 왜냐하면, 동기 함수는 항상 promise를 반환하기 때문이죠.
const mapLoop = async _ => {
console.log('Start')
const numFruits = await fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit)
return numFruit
})
console.log(numFruits)
console.log('End')
}
>'Start'
'[Promise, Promise, Promise]'
'End'
async/await을 사용할 경우 map은 항상 promise들을 반환하기 때문에, 여러분은 promise로 이루어진 배열이 resolve되기를 기다려야 합니다. await Promise.all(arrayOfPromises)
처럼요.
const mapLoop = async _ => {
console.log('Start')
const promises = fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit)
return numFruit
})
const numFruits = await Promise.all(promises)
console.log(numFruits)
console.log('End')
}
그러면 아래와 같은 결과를 얻을 수 있습니다.
'Start'
'[27, 0, 14]'
'End'
만약 반환되는 값에 추가적인 작업을 하고 싶다면, 아래와 같이 할 수 있습니다. 이렇게하면 resolve 될 값은 여러분이 callback함수 안에서 return하는 값이 됩니다.
const mapLoop = async _ => {
// ...
const promises = fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit)
// Adds onn fruits before returning
return numFruit + 100
})
// ...
}
'Start'
'[127, 100, 114]'
'End'
filter안의 await
filter
를 사용할때, 여러분은 배열을 특정한 결과 값으로 정제하고 싶을 것입니다. 여러분이 과일의 수가 20개 이상인 과일만 가진 배열을 만들고 싶다고 가정해 봅시다.
만약, await
없이 filter
를 쓰고 싶다면, 아마도 여러분은 아래와 같이 사용할 것입니다.
// Filter if there's no await
const filterLoop = _ => {
console.log('Start')
const moreThan20 = fruitsToGet.filter(fruit => {
const numFruit = fruitBasket[fruit]
return numFruit > 20
})
console.log(moreThan20)
console.log('End')
}
그리고나서 사과의 수가 27개 이기 때문에 moreThan20
의 값이 사과만 가지고 있을 것이라고 생각해볼 수 있겠죠.
'Start'
['apple']
'End'
하지만 filter
안에서 await
은 같은 방식으로 동작하지 않습니다. 사실은, 동작자체를 하지 않습니다. 그냥 정제되지 않은 배열을 그대로 다시 얻게될 뿐이에요.
const filterLoop = async _ => {
console.log('Start')
const moreThan20 = await fruitsToGet.filter(async fruit => {
const numFruit = await getNumFruit(fruit)
return numFruit > 20
})
console.log(moreThan20)
console.log('End')
}
'Start'
['apple', 'grape', 'pear']
'End'
자, 무슨일이 일어난 것인지 알아봅시다.filter
callback안에서 await을 사용하면, callback은 항상 promise를 반환합니다. promise는 항상 'truthy'이기 때문에 배열 안의 모든 요소들이 filter를 통과할 수 있는 것이죠. 아래 처럼요.
// Everything passes the filter...
const filtered = array.filter(true)
자, 그렇다면 filter
안에서 await
을 제대로 쓸 수있는 방법을 3단계로 나누어 봅시다.
-
promise배열을 반환 받기위해
map
을 사용합시다. -
await
을 이용해 반환받은 promise배열을 resolve 해줍니다. - resolved된 값들을
filter
을 이용해서 정제해줍니다.
const filterLoop = async _ => {
console.log('Start')
const promises = await fruitsToGet.map(fruit => getNumFruit(fruit))
const numFruits = await Promise.all(promises)
const moreThan20 = fruitsToGet.filter((fruit, index) => {
const numFruit = numFruits[index]
return numFruit > 20
})
console.log(moreThan20)
console.log('End')
}
Start
[ 'apple' ]
End
reduce안의 await
보통은 reduce
를 반복문을 통해 누적합을 구하고 싶을 때 사용하는 경우가 많기 때문에, 이번에는 여러분이 과일 바구니 안에 들어있는 과일의 총 개수를 구하고 싶다고 가정해봅시다.
// Reduce if there's no await
const reduceLoop = _ => {
console.log('Start')
const sum = fruitsToGet.reduce((sum, fruit) => {
const numFruit = fruitBasket[fruit]
return sum + numFruit
}, 0)
console.log(sum)
console.log('End')
}
이렇게하면 총 41개의 과일 수를 구할 수 있을 것입니다. (27 + 0 + 14 = 41).
'Start'
'41'
'End'
await
을 reduce
와 함께 쓴다면 어떻게 될까요? 결과는 엉망이 됩니다.
// Reduce if we await getNumFruit
const reduceLoop = async _ => {
console.log('Start')
const sum = await fruitsToGet.reduce(async (sum, fruit) => {
const numFruit = await getNumFruit(fruit)
return sum + numFruit
}, 0)
console.log(sum)
console.log('End')
}
'Start'
'[object Promise]14'
'End'
네? [object Promise]14
요?
자, 어떻게 된 일인지 알아보도록 합시다.
- 첫번째 반복에서
sum
은0
입니다.numFruit
은 27(getNumFruit('apple')에서 resolve된 값)입니다. 따라서
0 + 27`을 하면 27입니다. - 두번째 반복에서
sum
은 promise입니다.(왜일까요? 당연히 동기 함수는 항상 promise를 반환하니까요! 첫번째 반복에서의 accumulator(=sum + numFruit)이 promise로 반환되었기 때문입니다.)numFruit
은 0입니다. promise는 연산을 할 수 없습니다. 그렇기 때문에 자바스크립트는 promise를[obect Promise]
string으로 변환합니다. 그렇기 때문에[object Promise] + 0
은[object Promise]0
이 되는 것이죠. - 세번째 반복에서의
sum
또한 promise입니다.numFruit
은14
입니다.[object Promise] + 14
는 `[object Promise]14가 됩니다.
미스터리 해결!
그렇다면 어떻게 해야할까요?
reduce
callback 안에서 await
을 사용할 수는 있습니다. 하지만 accumulator
를 await
해주어야 한다는걸 잊지마세요!
const reduceLoop = async _ => {
console.log('Start')
const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
const sum = await promisedSum
const numFruit = await getNumFruit(fruit)
return sum + numFruit
}, 0)
console.log(sum)
console.log('End')
}
'Start'
'41'
'End'
원하는대로 값이 잘 나왔나요? 하지만 위의 gif파일에서도 보면 알 수 있듯이, 모든 것을 await하는데 꽤 많은 시간이 소요되는 군요.
이는 reduceLoop
이 promisedSum
이 각 반복에서 완료 될때까지 기다려야하기 때문에 발생하는 것입니다.
이러한 reduce 반복문의 속도를 빠르게하는 방법이 있습니다. 만약 await getNumFruits()
를 await promisedSum
의 전에 실행시킨다면, reduceLoop
은 코드를 처리하는데 긴 시간이 필요하지 않습니다.
const reduceLoop = async _ => {
console.log('Start')
const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
// Heavy-lifting comes first.
// This triggers all three `getNumFruit` promises before waiting for the next interation of the loop.
const numFruit = await getNumFruit(fruit)
const sum = await promisedSum
return sum + numFruit
}, 0)
console.log(sum)
console.log('End')
}
reduce
가 모든 세 getNumFruit
프라미스를 다음 반복을 기다리기 전에 실행시키기 때문에 속도가 빨라진 것입니다. 하지만, 이 방법은 await
을 사용할때 순서에 주의를 기울여야 하기 때문에 조금 혼란스럽기도 합니다.
가장 간단하고 효율적인 방법은 이렇게 사용하는 것입니다.
- 먼저 promise 배열을 반환 받기 위해
map
을 사용하세요. - 반환 받은 promise배열을
await Promise.all()
을 사용해서 resolve 된 값으로 반환받으세요. - resolved된 값에 reduce를 사용하세요.
const reduceLoop = async _ => {
console.log('Start')
const promises = fruitsToGet.map(getNumFruit)
const numFruits = await Promise.all(promises)
const sum = numFruits.reduce((sum, fruit) => sum + fruit)
console.log(sum)
console.log('End')
}
이렇게하면 가독성도 좋고, 간단하게 해결 할 수 있습니다. 그리고 모든 숫자를 계산하는데도 오래걸리지 않죠.
결론
await
을 반복해서 실행시키고 싶다면, for-loop이나 callback이 없는 반복문을 사용하세요.forEach
에서는await
을 절대 사용하지 마세요! 대신에 for-loop이나 callback이 없는 반복문을 사용하세요.filter
나reduce
안에서는await
을 사용하지 마세요. 항상map
으로 promise배열을 반환해주어서await
으로 resolve해준 뒤, 그 값으로filter
나reduce
를 사용하세요.
'JavaScript' 카테고리의 다른 글
자바스크립트에서 불변성(Immutability)이란 (5) | 2021.03.16 |
---|---|
ECMAScript 와 JavaScript의 차이점 (0) | 2019.10.21 |
자바스크립트 문자열, single or double quotes?... and backtick (1) | 2019.10.02 |
자바스크립트 reduce 안에서 async/await 쓰기 (0) | 2019.09.24 |
배열의 reduce() 파헤치기 (0) | 2019.08.22 |