๋ฐ๋ณต๋ฌธ์์ API ํธ์ถํ๊ธฐ โ forEach๊ฐ ์ ๋๋ ์ด์ , ๊ทธ๋ฆฌ๊ณ Promise.all์ ์คํด
YouTube Playlist Generator๋ฅผ
๋ง๋ค ๋, ๊ณก ๋ชฉ๋ก์ ์ํํ๋ฉฐ ๊ฐ ๊ณก์ ๊ฒ์ํ๊ณ ์ฌ์๋ชฉ๋ก์ ๋ฃ๋ ์ฝ๋๋ฅผ ์งฐ๋ค.
๊ทธ๋ฐ๋ฐ forEach๋ก ์งฐ๋๋ ์ด์ํ๊ฒ ๋์ํ๋ ์ฝ๋๊ฐ, for...of๋ก ๋ฐ๊พธ๋
๋ฉ์ฉกํ ๋์๊ฐ๋ค. ๊ทธ๋๋ "๋๋๊น ๋์ด๊ฐ์" ํ์ง๋ง, ๋ญ๊ฐ ๋ค๋ฅธ์ง ์ ๋๋ก
ํ๋ดค๋ค. ์์ ์ ์ด ์ฃผ์ ๋ก ๊ธ์ ํ ๋ฒ ์ผ๋๋ฐ, ๋ค์ ๊ฒ์ฆํด๋ณด๋ ๋ด๊ฐ ํ๋ ธ๋
๋ถ๋ถ๋ ์์๋ค. ์ด๋ฒ์ ์ค์ ๋ก ์ฝ๋๋ฅผ ๋๋ ค ํ์ธํ ๊ฒฐ๊ณผ๋ก ์ ๋ฆฌํ๋ค.
๊ฒ์ฆ์ฉ ์ฝ๋
์๋์ฒ๋ผ "API ํธ์ถ"์ ํ๋ด ๋ธ๋ค. id๊ฐ ํด์๋ก ๋นจ๋ฆฌ ๋๋๊ฒ ๋ง๋ค์ด์, ํธ์ถํ ์์์ ์๋ฃ๋๋ ์์๊ฐ ๋ค์์ด๋๋ก ํ๋ค.
const ids = [1, 2, 3, 4, 5];
const fakeApi = (id) =>
new Promise((res) => setTimeout(() => res(id), (6 - id) * 60));
1. forEach โ async๋ฅผ ๊ธฐ๋ค๋ ค์ฃผ์ง ์๋๋ค
๋ด๊ฐ ์ฒ์ ์ง ์ฝ๋๊ฐ ์ด๊ฑฐ์๋ค. (git ๊ธฐ๋ก์ ๋ณด๋ ์ง์ง๋ก ์ด๋ฌ๋ค ๐ )
console.time("forEach");
const results = [];
ids.forEach(async (id) => {
const r = await fakeApi(id);
results.push(r);
});
console.timeEnd("forEach");
console.log("forEach ์งํ:", results);
์ค์ ์ถ๋ ฅ:
forEach: 0.21ms
forEach ์งํ: []
(500ms ๋ค): [ 5, 4, 3, 2, 1 ]
console.timeEnd๊ฐ 0.21ms ๋ง์ ์ฐํ๋ค. forEach๋ ์ฝ๋ฐฑ์ด ๋๋ ค์ค
Promise๋ฅผ ๊ฑฐ๋ค๋ ๋ณด์ง๋ ์๊ธฐ ๋๋ฌธ์ด๋ค. ์ฝ๋ฐฑ์ ์ฃฝ ์คํ๋ง ํ๊ณ ๋ฐ๋ก
๋ค์ ์ค๋ก ๋์ด๊ฐ๋ค. ๊ทธ๋์ forEach ์งํ์ results๊ฐ ํ
๋น์ด ์๊ณ ,
ํ์ฐธ ๋ค์์ผ ์๋ฃ๋ ์์(5,4,3,2,1)๋๋ก ์ฑ์์ง๋ค.
๋ด ํ๋ก์ ํธ์์ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ก๋๋ฉด โ ๊ฒ์์ด ๋๋๊ธฐ๋ ์ ์ "๋ค์ ๋จ๊ณ"๊ฐ
์คํ๋ผ์, ์์ง ์ ๋ง๋ค์ด์ง ์ฌ์๋ชฉ๋ก์ ๊ณก์ ๋ฃ์ผ๋ ค๋ค ๊นจ์ก๋ค. forEach๊ฐ
await๋ฅผ ๊ธฐ๋ค๋ฆฐ๋ค๊ณ ์ฐฉ๊ฐํ ๊ฒ ์์ธ์ด์๋ค.
MDN๋ ๋ช ์ํ๋ค: forEach๋ promise๋ฅผ ๊ธฐ๋ค๋ฆฌ์ง ์๋๋ค("it does not wait for promises").
2. for...of โ ์์ฐจ์ ์ด๊ณ , ํ์คํ๊ณ , ๋๋ฆฌ๋ค
for...of๋ ๋ฃจํ ์์ await์์ ์ง์ง๋ก ๋ฉ์ถ๋ค. ํ๋ ๋๋์ผ ๋ค์์ผ๋ก
๊ฐ๋ค.
console.time("for...of");
const seq = [];
for (const id of ids) {
seq.push(await fakeApi(id));
}
console.timeEnd("for...of");
for...of: 904.705ms
๊ฒฐ๊ณผ: [ 1, 2, 3, 4, 5 ]
์์๋ ์๋ฒฝํ๊ณ ๋์๋ ํ์คํ๋ค. ๋์ ๋๋ฆฌ๋ค. ๊ฐ ํธ์ถ ์๊ฐ์ด ๊ทธ๋๋ก ํฉ์ฐ๋๋ค(60+120+โฆ+300 โ 900ms). ๊ณก์ด ๋ง์ผ๋ฉด ๋ต๋ตํ ๋งํผ ๋๋ ค์ง๋ค.
3. map + Promise.all โ ๋์์ ์๊ธฐ
map์ forEach์ ๋ฌ๋ฆฌ ์ฝ๋ฐฑ์ ๋ฐํ๊ฐ์ผ๋ก ์ ๋ฐฐ์ด์ ๋ง๋ ๋ค. async ์ฝ๋ฐฑ์
๋ฃ์ผ๋ฉด Promise๋ค์ ๋ฐฐ์ด์ด ๋์จ๋ค. ์ด๊ฑธ Promise.all๋ก ํ ๋ฒ์ ๊ธฐ๋ค๋ฆฐ๋ค.
console.time("Promise.all");
const done = [];
const promises = ids.map(async (id) => {
const r = await fakeApi(id);
done.push(r); // ์๋ฃ๋๋ ์์๋๋ก ์์ธ๋ค
return r;
});
const all = await Promise.all(promises);
console.timeEnd("Promise.all");
console.log("์๋ฃ๋ ์์:", done);
console.log("๊ฒฐ๊ณผ ๋ฐฐ์ด:", all);
Promise.all: 301.521ms
์๋ฃ๋ ์์: [ 5, 4, 3, 2, 1 ]
๊ฒฐ๊ณผ ๋ฐฐ์ด: [ 1, 2, 3, 4, 5 ]
for...of๊ฐ 904ms์ธ๋ฐ ์ด๊ฑด 301ms. ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ๋ค ์ด๋ฒ๋ฆฐ ๋ค ํ๊บผ๋ฒ์
๋ชจ์ผ๋, ์ ์ฒด ์๊ฐ์ด "๊ฐ์ฅ ์ค๋ ๊ฑธ๋ฆฌ๋ ํ๋(300ms)"์ ์๋ ดํ๋ค.
๊ทธ๋ ๋ด๊ฐ ํ๋ ธ๋ ๊ฒ: "Promise.all์ด ์์๋ฅผ ๋ณด์ฅํ๋ค"
์์ ๊ธ์์ ๋๋ "Promise.all์ด ์์๋ฅผ ๋ณด์ฅํ๋ค"๊ณ ๋ญ๋ฑ๊ทธ๋ ค ์ผ๋ค. ์ ๋ฐ๋ง ๋ง๋ ๋ง์ด์๋ค. ์ ์ถ๋ ฅ์ด ์ ํํ ๋ณด์ฌ์ค๋ค.
- ์๋ฃ๋๋ ์์๋ ๋ณด์ฅ๋์ง ์๋๋ค โ
done = [5, 4, 3, 2, 1](๋นจ๋ฆฌ ๋๋ ๊ฒ ๋จผ์ ์์ธ๋ค) Promise.all์ด ๋๋ ค์ฃผ๋ ๊ฒฐ๊ณผ ๋ฐฐ์ด์ ์์๋ ์ ๋ ฅ ์์ ๊ทธ๋๋ก๋ค โall = [1, 2, 3, 4, 5]
์ฆ Promise.all์ ์คํ์ ์์๋๋ก ํ๋ ๊ฒ ์๋๋ผ, ๊ฒฐ๊ณผ๋ฅผ ์
๋ ฅ ์์์
๋ง์ถฐ ์ ๋ ฌํด์ ๋๋ ค์ค ๋ฟ์ด๋ค. ์คํ ์์ฒด๋ ๋์์ ํฉ์ด์ง๋ค. ์์ ์ ๋๋
์ด ๋์ ํท๊ฐ๋ ธ๋ค.
๊ทธ๋ฆฌ๊ณ ํ๋ ๋. ํํ "๋ณ๋ ฌ(parallel)"์ด๋ผ๊ณ ํ์ง๋ง, JavaScript๋ ์ฑ๊ธ ์ค๋ ๋๋ค. CPU๊ฐ ์ฌ๋ฌ ๊ฐ๋ก ๋๋ ๊ฒ ์๋๋ผ, ๋คํธ์ํฌ I/O๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ์๊ฐ์ด ๊ฒน์น๋ ๊ฒ๋ฟ์ด๋ผ ์ ํํ๋ "๋์(concurrent)"์ ๊ฐ๊น๋ค.
์ง์ง ๊ตํ: ๋น ๋ฅธ ๊ฒ ํญ์ ์ ๋ต์ ์๋๋ค
์ฌ๊ธฐ๊น์ง๋ง ๋ณด๋ฉด "๊ทธ๋ผ ๋ฌด์กฐ๊ฑด map + Promise.all์ด ์ต๊ณ ๋ค?" ์ถ๋ค. ๊ทธ๋ฐ๋ฐ ๋ด ํ๋ก์ ํธ์ ์ต์ข ์ฝ๋๋ ์ด๋ ๊ฒ ๋๋์ด ์๋ค.
- ๊ณก ๊ฒ์(์ฝ๊ธฐ) โ
map + Promise.all(๋์, ๋น ๋ฆ) - ์ฌ์๋ชฉ๋ก์ ๊ณก ์ถ๊ฐ(์ฐ๊ธฐ) โ
for๋ฃจํ +await(์์ฐจ)
// ๊ฒ์: ๋์์
const result = await Promise.all(checkValue.map((song) => searchVideo(...)));
// ์ถ๊ฐ: ํ๋์ฉ ์์ฐจ์ ์ผ๋ก
for (let i = 0; i < songArr.length; i++) {
await addToPlayList(accessToken, playlistId, songArr[i].videoId);
setProgress({ done: i + 1, total: songArr.length });
}
์ ์ถ๊ฐ๋ ์์ฐจ๋ก ํ๋๋ฉด โ ์ฌ์๋ชฉ๋ก ์ถ๊ฐ๋ ๊ฐ์ ์ฌ์๋ชฉ๋ก์ ์ฌ๋ฌ ์์ฒญ์ด
๋์์ ์์ ํ๋ ์ฐ๊ธฐ ์์
์ด๊ธฐ ๋๋ฌธ์ด๋ค. YouTube์ playlistItems.insert๋ฅผ
๋์์ ์ฌ๋ฌ ๊ฐ ์๋ฉด ๊ฐ์ ๋ฆฌ์์ค๋ฅผ ๋์์ ๊ณ ์น๋ ค๋ค 409 CONFLICT๋ก ์คํจํ๊ธฐ
์ฝ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฌด์๋ณด๋ค ์ฌ์๋ชฉ๋ก์ ๋ฃ๋ ์์๊ฐ ๊ฒฐ๊ณผ๋ฌผ์ด๋ค โ ๋์์ ์๋ฉด
์๋ฃ ์์๊ฐ ๋ค์์ฌ์ ๊ณก ์์๋ฅผ ๋ณด์ฅํ ์ ์๋ค. ๊ทธ๋์ ์ค์ ์ฝ๋๋ for ๋ฃจํ
์์์ ํ๋์ฉ awaitํ๋ฉฐ, ํ ๊ณก ๋๋ ๋๋ง๋ค ์งํ๋ฅ ์ ๊ฐฑ์ ํ๋ ๊ตฌ์กฐ๋ค.
๊ทธ๋์ ๊ฒฐ๋ก ์ ์ด๋ ๊ฒ ์ ๋ฆฌ๋๋ค.
| ์ํฉ | ์ ํ | ์ด์ |
|---|---|---|
| ์์ยท์ฑ๊ณต์ด ์ค์, ์๋ | for...of + await |
ํ์คํจ |
| ๋ค์์ ์ฝ๊ธฐ, ์๋ ์ค์ | map + Promise.all |
๋์ ์ฒ๋ฆฌ๋ก ๋น ๋ฆ |
| ์ฐ๊ธฐ๋ฅผ ๋๋์ผ๋ก | ์์ฐจ or ๋์์ฑ ์ ํ | ๋์ ์์ ์ถฉ๋ยท์์ ๋ณด์ฅ |
| async๋ฅผ ์ํ | forEach๋ ์ฐ์ง ๋ง ๊ฒ |
await๋ฅผ ์ ๊ธฐ๋ค๋ฆผ |
๋ค์ผ๋ก ์์๋ ๊ฒ: Promise.all์ ํ๋๋ง ์คํจํด๋ ์ ์ฒด๊ฐ ์ฆ์ ์คํจํ๋ค
(fail-fast). "์ผ๋ถ ์คํจ๋ ๋์ด๊ฐ๊ณ ์ถ๋ค"๋ฉด Promise.allSettled, "๋์์
๋๋ฌด ๋ง์ด ์๋ ๊ฒ ๋ฌธ์ "๋ผ๋ฉด ํ ๋ฒ์ N๊ฐ์ฉ๋ง ์ฒ๋ฆฌํ๋ ๋์์ฑ ์ ํ์
์ฐ๋ฉด ๋๋ค.
์ ๋ฆฌ
forEach+ async๋ await๋ฅผ ๊ธฐ๋ค๋ฆฌ์ง ์๋๋ค. ์ํ ๋น๋๊ธฐ์ ์ฐ์ง ๋ง์.for...of+ await๋ ์์ฐจ์ ยทํ์คํ์ง๋ง ๋๋ฆฌ๋ค.map+Promise.all์ ๋์์ ์ฒ๋ฆฌํด ๋น ๋ฅด๊ณ , ๊ฒฐ๊ณผ ๋ฐฐ์ด์ ์์๋ ์ ๋ ฅ์์ผ๋ก ๋ณด์ฅํ๋ค. (๋จ, ์คํ/์๋ฃ ์์๋ ์๋๋ค.)- ๊ทธ๋ฆฌ๊ณ ์ ์ผ ์ค์ํ ๊ฒ โ ์ฝ๊ธฐ๋ ๋์์, ์ฐ๊ธฐ๋ ์กฐ์ฌ์ค๋ฝ๊ฒ. ๋น ๋ฅธ ๊ฒ ๋ ์ณ์ ๊ฑด ์๋์๋ค.
์์ ์ ๋๋ "map์ด ์ ์ผ ๋น ๋ฅด๋ ์ต๊ณ "๋ผ๊ณ ๊ฒฐ๋ก ๋์ง๋ง, ์ค์ ๋ก ์๋น์ค๋ฅผ ๊ตด๋ ค๋ณด๋ ๋ต์ ์ํฉ๋ง๋ค ๋ฌ๋๋ค. ํ๋ ธ๋ ๊ฑธ ๋ค์ ์ง์ด๋ณด๋ ๊ฒ๋ ์ฑ์ฅ์ด๋ผ ๋ฏฟ๋๋ค.
- ๊ด๋ จ ํ๋ก์ ํธ: YouTube Playlist Generator