C-7: Promise 심화와 async/await
안녕하세요, 홍순구 튜터입니다. 지난 시간 우리는 비동기의 기초와 Promise를 배웠어요. "시간이 걸리는 일은 맡겨두고 다음 줄로 넘어간다"는 비동기의 본질을 잡았고, 콜백 지옥을 Promise 체이닝으로 평평하게 폈죠. 그리고 마지막에 두 가지를 약속하고 헤어졌어요.
첫째, "사진 세 장을 동시에 올리고, 셋 다 끝나면 다음으로 넘어가고 싶다"는 상황을 풀어주겠다고 했죠. Promise를 하나씩 then으로 잇는 것만으로는 부족했으니까요. 둘째, then을 줄줄이 잇는 체이닝을 마치 동기 코드처럼 위에서 아래로 읽히게 하는 방법, async/await를 보여주겠다고 했어요.
오늘 그 두 약속을 모두 지킵니다. 여러 Promise를 한꺼번에 다루는 네 가지 도구(Promise.all·allSettled·race·any)부터 시작해서, 비동기를 동기처럼 읽게 해주는 async/await, 그리고 순차와 병렬 중 무엇을 골라야 하는지까지 갑니다.
지난 시간 (C-6) 오늘 (C-7)
┌──────────────────────────┐ ┌──────────────────────────┐
│ Promise 하나를 then 으로 처리 │ ──▶ │ 여러 Promise 를 한꺼번에 │
│ 콜백 지옥 → 체이닝으로 평탄화 │ │ all/allSettled/race/any │
│ 성공·실패·마무리 나눠 다룸 │ │ async/await 로 동기처럼 읽기 │
└──────────────────────────┘ └──────────────────────────┘
오늘 코드도 화면(DOM)을 아직 건드리지 않아요. 결과는 전부 콘솔(console.log)로 확인합니다. "여러 개의 기다림을 어떻게 묶을까"에 익숙해지는 시간이에요.
💡 오늘 수업의 핵심 — "여러 비동기를 묶을 땐 목적에 맞는 도구를 고른다. 다 모으면 all, 첫 성공이면 race·any, 부분 실패를 허용하면 allSettled. 그리고 then 체이닝은 async/await로 동기처럼 읽는다." 🎯
🎯 학습 목표
Promise.all로 여러 비동기를 동시에 시작하고, 셋 다 끝나면 결과를 한꺼번에 받습니다.Promise.allSettled로 하나가 실패해도 나머지를 끝까지 챙기는 법을 익힙니다.Promise.race(첫 결판)와Promise.any(첫 성공)의 차이를 구분합니다.- async/await로 then 체이닝을 위에서 아래로 읽히는 코드로 바꿉니다.
- await를 연달아 쓰는 순차 처리와
Promise.all+ await의 병렬 처리를 비교합니다. - try-catch-finally로 async/await의 에러를 동기 코드처럼 처리합니다.
- 순차와 병렬을 언제 골라야 하는지 판단 기준을 세웁니다.
Step 1: "사진 세 장을 동시에" — Promise.all
지난 시간 마지막에 약속한 그 상황부터 풀어볼게요. 사진 세 장을 한꺼번에 업로드하고, 셋 다 끝났을 때 다음으로 넘어가고 싶어요. Promise를 하나씩 then으로 이으면 첫 장이 끝나야 둘째 장을 시작하니, 동시에 올리는 게 아니죠. 이럴 때 쓰는 게 Promise.all이에요.
먼저 오늘 내내 쓸 도우미 함수를 하나 소개할게요. "사진 한 장 업로드"를 흉내 내는 가짜 Promise예요. 진짜 서버 대신 setTimeout으로 "시간이 걸리는 일"을 흉내 내요.
// instagram-clone-frontend/js/async-advanced.js
function uploadPhoto(name, delay, success = true) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve(`${name} 업로드 완료`);
} else {
reject(`${name} 업로드 실패`);
}
}, delay);
});
}
name(사진 이름), delay(걸리는 시간, 밀리초), success(성공할지 여부)를 받아요. success가 true면 delay만큼 기다린 뒤 성공하고, false면 실패해요. 오늘 모든 예제가 이 함수를 재료로 써요.
이제 Promise.all로 세 장을 동시에 올려볼게요.
function demoAll() {
return Promise.all([
uploadPhoto("photo1.jpg", 300),
uploadPhoto("photo2.jpg", 500),
uploadPhoto("photo3.jpg", 400),
])
.then((results) => {
console.log("[all] 세 장 모두 끝남:", results);
})
.catch((error) => {
console.log("[all] 하나라도 실패:", error);
});
}
Promise.all은 Promise 배열을 받아요. 배열 속 세 개를 한꺼번에 시작하고, 셋 다 끝나면 결과를 배열로 모아서 then으로 넘겨줘요. 콘솔엔 이렇게 찍혀요.
[all] 세 장 모두 끝남: [ 'photo1.jpg 업로드 완료', 'photo2.jpg 업로드 완료', 'photo3.jpg 업로드 완료' ]
여기서 중요한 점이 하나 있어요. 세 장의 시간이 각각 300·500·400밀리초인데, 동시에 시작했으니 전체는 셋 중 가장 느린 500밀리초면 끝나요. 하나씩 이었다면 300+500+400으로 1.2초가 걸렸을 텐데요. 동시에 시작하면 그만큼 빨라지는 거예요.
그리고 Promise.all에는 까칠한 성격이 하나 있어요. 하나라도 실패(reject)하면 그 즉시 catch로 가요. 나머지가 끝나길 기다리지 않고요. 셋 중 한 장이라도 업로드에 실패하면, 전체를 실패로 보고 바로 catch가 받아요. "모 아니면 도"인 셈이죠. 이 까칠함이 문제가 될 때가 있는데, 그걸 다음 Step에서 풀어요.
💡
Promise.all은 "전부 다 성공해야 의미가 있는 일"에 어울려요. 세 장을 모두 올려야 게시물이 완성되는 경우처럼요. 단, 하나라도 실패하면 즉시 전체가 실패예요.
Step 2: "하나 실패해도 끝까지" — Promise.allSettled
방금 Promise.all의 까칠함을 봤죠. 하나만 실패해도 전체가 실패로 튕겨요. 그런데 이런 상황을 떠올려보세요. 사진 10장을 한꺼번에 올리는데, 그중 1장만 실패했어요. Promise.all이라면 나머지 9장이 잘 올라갔어도 전체가 실패가 돼버려요. 너무 아깝죠.
이럴 때 쓰는 게 Promise.allSettled예요. 이름의 "settled"는 "결판이 났다"는 뜻이에요. 성공이든 실패든 전부 끝날 때까지 기다려요. 중간에 누가 실패해도 멈추지 않고요.
// instagram-clone-frontend/js/async-advanced.js
function demoAllSettled() {
return Promise.allSettled([
uploadPhoto("a.jpg", 300),
uploadPhoto("b.jpg", 400, false), // 이 한 장만 실패
uploadPhoto("c.jpg", 500),
]).then((results) => {
results.forEach((r) => {
if (r.status === "fulfilled") {
console.log("[allSettled] 성공:", r.value);
} else {
console.log("[allSettled] 실패:", r.reason);
}
});
});
}
가운데 b.jpg만 일부러 실패하게 했어요(세 번째 인자 false). Promise.all이었다면 여기서 바로 catch로 갔겠지만, allSettled는 셋 다 끝까지 기다려요. 콘솔엔 이렇게 찍혀요.
[allSettled] 성공: a.jpg 업로드 완료
[allSettled] 실패: b.jpg 업로드 실패
[allSettled] 성공: c.jpg 업로드 완료
실패한 b.jpg만 따로 알려주고, 성공한 a.jpg·c.jpg는 멀쩡히 챙겼어요.
여기서 결과 모양이 all과 달라요. allSettled는 각 결과를 객체로 줘요. 성공이면 { status: "fulfilled", value: 값 }, 실패면 { status: "rejected", reason: 이유 }예요. 그래서 r.status로 성공·실패를 가르고, 성공이면 r.value, 실패면 r.reason을 꺼내요.
Promise.all Promise.allSettled
───────────────── ─────────────────────
하나 실패 → 즉시 전체 실패 전부 끝까지 기다림
성공 결과를 배열로 { status, value/reason } 배열로
"모 아니면 도" "되는 것만이라도 챙기자"
💡
all은 "전부 성공이 필요할 때",allSettled는 "일부 실패를 감수하고 나머지를 챙길 때"예요. 10장 중 9장이라도 올리고 싶다면allSettled죠.
Step 3: "가장 빠른 것 / 첫 성공" — Promise.race / Promise.any
all과 allSettled는 "전부"를 기다렸어요. 그런데 반대로, 하나만 필요할 때도 있어요. 예를 들어 같은 데이터를 서버 여러 곳에서 받아올 수 있다면, 굳이 다 기다릴 필요 없이 가장 빨리 답한 곳 하나면 충분하죠. 이럴 때 쓰는 게 Promise.race와 Promise.any예요. 둘은 비슷해 보이지만 결정적으로 달라요.
먼저 Promise.race예요. "race"는 경주죠. 성공이든 실패든 상관없이 가장 먼저 끝난 하나로 결판이 나요.
// instagram-clone-frontend/js/async-advanced.js
function demoRace() {
return Promise.race([
uploadPhoto("서버A", 500),
uploadPhoto("서버B", 200), // 가장 빠름
uploadPhoto("서버C", 800),
]).then((winner) => console.log("[race] 가장 빠른 응답:", winner));
}
서버A·B·C가 각각 500·200·800밀리초가 걸려요. 가장 빠른 건 200밀리초인 서버B죠. 실행해보면 이렇게 찍혀요.
[race] 가장 빠른 응답: 서버B 업로드 완료
서버B가 가장 빨리 끝났으니 서버B로 결판이에요. 여기서 주의할 점은, race는 성공·실패를 가리지 않는다는 거예요. 만약 가장 빠른 게 실패였다면, 그 실패로 결판이 나서 catch로 갔을 거예요. "먼저 끝난 놈이 임자"인 셈이죠.
이번엔 Promise.any예요. any는 "가장 먼저 성공한" 하나를 줘요. 실패는 무시하고 성공을 기다린다는 게 race와의 결정적 차이예요.
function demoAny() {
return Promise.any([
uploadPhoto("서버A", 300, false), // 가장 빠르지만 실패 → 무시
uploadPhoto("서버B", 500),
uploadPhoto("서버C", 400), // 성공한 것 중 가장 빠름 → 이게 당첨
]).then((first) => console.log("[any] 가장 먼저 성공:", first));
}
여기서 서버A가 300밀리초로 가장 빠르지만, 일부러 실패하게 했어요(false). race였다면 가장 빠른 서버A의 실패로 결판이 났겠지만, any는 실패를 무시해요. 그래서 성공한 것 중 가장 빠른 서버C(400밀리초)가 당첨돼요.
[any] 가장 먼저 성공: 서버C 업로드 완료
서버A의 실패는 무시되고, 성공한 서버B(500)·서버C(400) 중 더 빠른 서버C가 뽑혔죠.
Promise.race Promise.any
───────────────── ─────────────────
먼저 끝난 하나 (성공·실패 무관) 먼저 성공한 하나 (실패는 무시)
빠른 게 실패면 → 실패로 결판 빠른 게 실패면 → 건너뛰고 성공 기다림
🙋 학생 질문 — "튜터님, race랑 any 둘 다 '가장 빠른 거'잖아요. 뭐가 다른 거예요?"
좋은 질문이에요. 한 단어로 정리하면 race는 "결판", any는 "성공"이에요.
race는 성공이든 실패든 따지지 않고, 가장 먼저 끝난 놈으로 끝나요. 그래서 가장 빠른 게 실패하면 그 실패로 결판이 나서 catch로 가요. 위 데모에서 서버A가 가장 빨리 실패했다면, race는 서버A의 실패를 받았을 거예요.
반면 any는 실패엔 관심이 없어요. 오직 "가장 먼저 성공한 하나"를 기다려요. 그래서 가장 빠른 게 실패해도 무시하고, 다음으로 빨리 성공한 걸 줘요. 위 데모에서 서버A가 실패해도 any는 신경 안 쓰고 서버C(성공 중 가장 빠름)를 골랐죠. "타임아웃 경주"엔 race, "어디든 한 곳만 성공하면 OK"엔 any라고 기억하면 편해요.
Step 4: "동기처럼 읽고 싶다" — async/await 소개
지금까지 우리는 비동기를 then으로 다뤘어요. then 안에 콜백을 넣고, 그게 돌려준 값을 다음 then으로 잇고요. 이게 콜백 지옥보다는 훨씬 낫지만, 여전히 한 가지 아쉬움이 있어요. 눈으로 읽기가 어렵다는 점이에요.
우리가 코드를 읽을 때 가장 편한 건 "위에서 아래로" 한 줄씩 읽는 거예요. 그런데 then 체이닝은 콜백 안으로 들어갔다 나왔다 하면서 흐름이 꺾여요. "이 값이 어디서 와서 어디로 가는 거지?" 하고 한 번 더 생각하게 되죠.
그래서 나온 게 async/await예요. then으로 이으면 동작은 맞지만 읽기 어려우니, 동기 코드처럼 위에서 아래로 읽고 싶다는 바람에서 태어났어요. 단, 속은 여전히 Promise예요. 모양만 바뀌는 거지, 비동기가 사라지는 게 아니에요.
같은 일을 두 방법으로 써서 비교해볼게요. 먼저 우리에게 익숙한 then 체이닝이에요.
// instagram-clone-frontend/js/async-advanced.js
function loadProfileThen() {
return uploadPhoto("프로필", 200)
.then((a) => {
console.log("[then]", a);
return uploadPhoto("피드", 200);
})
.then((b) => console.log("[then]", b));
}
프로필을 먼저 불러오고, 그게 끝나면 피드를 불러와요. then 안에서 다음 일을 return하고, 그 결과를 또 다음 then이 받죠. 동작은 완벽하지만 콜백 안으로 들어갔다 나오는 흐름이 보이나요?
이번엔 똑같은 일을 async/await로 써볼게요.
async function loadProfileAsync() {
const a = await uploadPhoto("프로필", 200);
console.log("[await]", a);
const b = await uploadPhoto("피드", 200);
console.log("[await]", b);
}
규칙은 딱 두 개예요. 함수 앞에 async를 붙이고, 기다릴 곳에 await를 붙여요. await uploadPhoto(...)는 "이 Promise가 끝날 때까지 기다렸다가, 결과 값을 꺼내서 a에 담아라"는 뜻이에요. then 콜백 없이, 그냥 변수에 결과가 담겨요.
두 함수의 콘솔 출력은 똑같아요.
[then] 프로필 업로드 완료
[then] 피드 업로드 완료
(async 버전은 [await]로 찍히겠죠.) 결과는 같고, 읽는 방식만 다른 거예요. async/await 버전을 보세요. 콜백도, 들여쓰기도 없이 위에서 아래로 평범한 코드처럼 읽혀요. const a = await ..., const b = await ... — 마치 동기 코드 같죠? 이게 async/await의 매력이에요.
💡 async/await는 새로운 비동기 기술이 아니에요. Promise를 더 읽기 좋게 입히는 옷이에요. 속은 그대로 Promise라서, 지난 시간에 배운 게 그대로 살아 있어요.
Step 5: await를 연달아 — 순차 처리
async/await의 진짜 편리함은 여러 단계를 이을 때 드러나요. await를 줄줄이 쓰면 "앞이 끝나야 다음"이 자연스럽게 표현돼요. 사진 세 장을 차례로 올리는 코드를 봐요.
// instagram-clone-frontend/js/async-advanced.js
async function uploadAllSequential() {
console.time("순차");
const r1 = await uploadPhoto("1번", 1000);
console.log("[순차]", r1);
const r2 = await uploadPhoto("2번", 1000);
console.log("[순차]", r2);
const r3 = await uploadPhoto("3번", 1000);
console.log("[순차]", r3);
console.timeEnd("순차"); // 약 3초
}
한 장당 1초(1000밀리초)가 걸려요. await가 세 번 줄줄이 있죠? 첫 줄 await에서 1초를 기다리고, 끝나야 둘째 줄로 가서 또 1초, 셋째 줄에서 또 1초예요. 그래서 전체가 약 3초가 걸려요. console.time과 console.timeEnd는 그 사이에 걸린 시간을 재주는 도구예요. 실행하면 콘솔에 순차: 3000ms 비슷하게 찍혀요.
이게 손해처럼 보일 수 있어요. "왜 한 장씩 기다려? 동시에 올리면 빠를 텐데." 맞아요. 하지만 앞 결과가 다음 단계에 꼭 필요할 때는 어쩔 수 없이 순차로 가야 해요. 예를 들어 "로그인해서 받은 토큰으로 내 정보를 조회하고, 그 정보로 게시물을 불러온다"면, 앞이 끝나야 다음을 시작할 수 있죠. 이렇게 단계가 서로 의존할 때 순차 처리가 필요해요.
💡 순차 처리는 "앞 결과가 있어야 다음을 할 수 있을 때" 쓰는 방식이에요. 느린 게 아니라, 순서가 꼭 필요한 거예요.
Step 6: Promise.all + await — 병렬 처리
그렇다면 단계가 서로 의존하지 않을 때는요? 사진 세 장은 서로 아무 상관이 없어요. 1번 사진이 올라가야 2번을 시작할 수 있는 게 아니죠. 이럴 때 순차로 가면 3초나 까먹어요. 동시에 시작하면 되는데요.
여기서 Step 1의 Promise.all과 async/await를 합쳐요. 한꺼번에 시작하고, 한 번만 await로 기다리면 돼요.
// instagram-clone-frontend/js/async-advanced.js
async function uploadAllParallel() {
console.time("병렬");
const results = await Promise.all([
uploadPhoto("1번", 1000),
uploadPhoto("2번", 1000),
uploadPhoto("3번", 1000),
]);
console.log("[병렬]", results);
console.timeEnd("병렬"); // 약 1초
}
세 장을 Promise.all로 묶어 한꺼번에 시작하고, 그 전체를 await 한 번으로 기다려요. 세 장이 동시에 1초씩 흐르니까, 전체는 약 1초면 끝나요. Step 5의 순차 버전과 코드를 나란히 놓고 실행해보면 차이가 또렷이 갈려요. 순차는 3초, 병렬은 1초로요.
순차 (uploadAllSequential) 병렬 (uploadAllParallel)
─────────────────────── ──────────────────────
1번 ████ (1초) 1번 ████ ┐
2번 ████ (1초) 2번 ████ ├ 동시에
3번 ████ (1초) 3번 ████ ┘
합계 ≈ 3초 합계 ≈ 1초
같은 세 장을 올리는데 시간이 3배 차이가 나죠. 이 차이가 곧 사용자가 느끼는 "느린 앱 / 빠른 앱"으로 이어져요. 핵심은 이거예요. 서로 의존하지 않는 일은 동시에 시작하라. Step 5의 순차와 Step 6의 병렬, 둘 중 무엇을 고를지가 다음 Step의 주제예요.
💡 병렬 처리는 "서로 상관없는 일을 동시에" 시작해 시간을 아끼는 방식이에요.
Promise.all을await한 번으로 감싸면 끝이에요.
Step 7: try-catch-finally — async/await의 에러 처리
비동기에서 에러 처리는 빠질 수 없어요. then 체이닝에선 .catch로 받았죠. 그런데 async/await에선 어떻게 할까요? async/await가 "동기 코드처럼" 읽힌다고 했으니, 에러 처리도 동기 코드와 똑같이 해요. 바로 try-catch예요.
// instagram-clone-frontend/js/async-advanced.js
async function uploadWithCatch() {
try {
const result = await uploadPhoto("중요사진", 300, false); // 실패하는 업로드
console.log("[try]", result);
} catch (error) {
console.log("[catch] 에러 잡음:", error);
} finally {
console.log("[finally] 로딩 표시 끄기");
}
}
일부러 실패하는 업로드를 넣었어요(false). await한 Promise가 실패하면, 그 에러가 catch 블록으로 튀어요. 동기 코드에서 에러가 났을 때 catch가 잡는 것과 똑같죠. 실행하면 이렇게 찍혀요.
[catch] 에러 잡음: 중요사진 업로드 실패
[finally] 로딩 표시 끄기
try 안에서 실패했으니 console.log("[try]", result)는 건너뛰고 바로 catch로 갔어요. 그리고 finally는 성공이든 실패든 항상 실행돼요. 지난 시간 then/catch/finally의 finally와 똑같은 역할이에요. "로딩 표시 끄기"처럼, 결과와 상관없이 꼭 해야 할 마무리에 어울려요.
then 체이닝 async/await
────────────── ──────────────
.then(성공) try { await ... 성공 }
.catch(실패) catch (e) { 실패 }
.finally(마무리) finally { 마무리 }
then 체이닝과 async/await는 모양만 다르지 하는 일은 똑같아요. .then→try, .catch→catch, .finally→finally로 1:1 대응되죠. 동기 코드를 다루던 try-catch가 비동기에도 그대로 통한다는 게 async/await의 큰 장점이에요.
Step 8: 순차냐 병렬이냐 — 선택의 기준
Step 5와 Step 6에서 순차(약 3초)와 병렬(약 1초)을 봤어요. 시간만 보면 병렬이 무조건 좋아 보이는데, 항상 병렬을 쓸 수 있는 건 아니에요. 이번엔 코드 없이, 둘을 언제 골라야 하는지 정리하고 가요.
두 방식을 타임라인으로 나란히 그려볼게요.
순차 처리 (앞이 끝나야 다음)
──────────────────────────────────────────────
로그인 ████ (1초)
내 정보 ████ (1초) ← 로그인 토큰이 있어야 시작 가능
게시물 ████ (1초) ← 내 정보(ID)가 있어야 시작 가능
├──────────────── 약 3초 ────────────────┤
병렬 처리 (서로 상관없이 동시에)
──────────────────────────────────────────────
좋아요 ████ ┐
댓글 ████ ├ 동시에 시작 (서로 의존 없음)
공유 ████ ┘
├─── 약 1초 ───┤
차이가 보이죠? 위쪽은 화살표가 줄줄이 이어져요. 로그인 토큰이 있어야 내 정보를 받고, 내 정보의 ID가 있어야 게시물을 받아요. 앞 결과가 다음의 재료라서 동시에 시작할 수가 없어요. 반대로 아래쪽 좋아요·댓글·공유는 서로 아무 상관이 없어서 한꺼번에 시작할 수 있어요.
판단 기준을 정리하면 이래요.
- 앞 결과가 다음 단계에 필요한가? → 그렇다면 순차 (await를 연달아). 의존이 있으니 어쩔 수 없어요.
- 서로 상관없는 독립적인 일인가? → 그렇다면 병렬 (
Promise.all+ await). 동시에 시작해 시간을 아껴요. - 여러 곳 중 가장 빠른 하나, 또는 첫 성공 하나면 충분한가? → race(첫 결판) 또는 any(첫 성공).
💡 "병렬이 빠르니까 무조건 병렬"이 아니에요. 의존이 있으면 순차밖에 못 써요. 먼저 "이 일들이 서로 의존하나?"를 물어보고, 독립적이면 그때 병렬로 묶어 시간을 아끼세요.
Step 9: 종합 — 좋아요·댓글·공유 동시에 불러오기
오늘 배운 걸 모두 모아볼게요. 게시물 하나를 화면에 띄울 때, 그 게시물의 좋아요 수·댓글 목록·공유 수를 함께 가져와야 한다고 해봐요. 이 셋은 서로 상관이 없죠. 좋아요 수가 있어야 댓글을 받는 게 아니니까요. 그러니 Step 8의 기준대로 병렬이 정답이에요.
먼저 세 가지 데이터를 가져오는 가짜 Promise 세 개를 준비해요.
// instagram-clone-frontend/js/async-advanced.js
function fetchLikes() {
return new Promise((resolve) => setTimeout(() => resolve(1240), 300));
}
function fetchComments() {
return new Promise((resolve) => setTimeout(() => resolve(["멋져요!", "최고"]), 400));
}
function fetchShares() {
return new Promise((resolve) => setTimeout(() => resolve(58), 200));
}
각각 좋아요 수(1240), 댓글 배열(["멋져요!", "최고"]), 공유 수(58)를 시간차를 두고 돌려줘요. 이제 이 셋을 한꺼번에 불러와요.
async function loadPostInteractions() {
try {
const [likes, comments, shares] = await Promise.all([
fetchLikes(),
fetchComments(),
fetchShares(),
]);
console.log(`[종합] 좋아요 ${likes}개, 댓글 ${comments.length}개, 공유 ${shares}회`);
} catch (error) {
console.log("[종합] 불러오기 실패:", error);
}
}
오늘 배운 게 다 들어 있어요. Promise.all로 셋을 동시에 시작하고(Step 1·6), await로 한 번에 기다리고(Step 4), try-catch로 에러를 감싸고(Step 7), 결과 배열을 [likes, comments, shares]로 풀어요.
이 마지막 부분이 지난 함수·배열 시간에 배운 구조 분해(destructuring)예요. Promise.all이 돌려준 결과 배열의 0·1·2번째를 각각 변수로 한 번에 받는 거죠.
콘솔엔 이렇게 찍혀요.
[종합] 좋아요 1240개, 댓글 2개, 공유 58회
셋이 동시에 시작했으니 전체는 가장 느린 400밀리초면 끝나요. 그리고 구조 분해 덕분에 결과를 깔끔하게 이름 붙여 꺼냈죠. 좋아요 1240개, 댓글 2개(comments.length), 공유 58회가 한 줄로 정리됐어요.
자, 이 셋을 받아냈으니 이제 진짜 할 일은 하나 남았어요. 이 숫자들을 화면에 그리는 것이에요. 지금까진 콘솔로만 결과를 봤는데, 그다음 단계가 우리를 기다리고 있어요.
마무리
오늘 우리는 여러 Promise를 한꺼번에 다루는 도구들과, 비동기를 동기처럼 읽게 해주는 async/await를 배웠어요. 되짚어볼게요.
- Promise.all — 여러 개를 동시에 시작하고, 셋 다 끝나면 결과를 배열로 받아요. 단, 하나라도 실패하면 즉시 전체 실패.
- Promise.allSettled — 성공·실패 상관없이 전부 끝까지 기다려요. 부분 실패를 감수하고 나머지를 챙길 때.
- Promise.race — 성공·실패 무관, 가장 먼저 끝난 하나로 결판.
- Promise.any — 실패는 무시하고, 가장 먼저 성공한 하나.
- async/await — then 체이닝을 위에서 아래로 읽히게 입힌 옷. 속은 그대로 Promise.
- 순차 vs 병렬 — 앞 결과가 필요하면 순차(await 연달아), 독립적이면 병렬(
Promise.all+await). - try-catch-finally — async/await의 에러는 동기 코드처럼 try-catch로.
고를 때 기준은 한 줄로 외워두세요. 다 모으면 all, 첫 성공이면 race·any, 부분 실패를 허용하면 allSettled, 읽기 좋게는 async/await. 처음엔 종류가 많아 헷갈릴 수 있지만, "지금 내가 전부가 필요한가, 하나만 필요한가, 실패를 견딜 수 있는가"를 물어보면 자연스럽게 도구가 골라져요.
다음 시간 예고
여기까지 오면서 우리는 비동기로 데이터를 받아내는 법을 다 배웠어요. 그런데 한 가지 답답한 점이 있었죠. 결과를 전부 콘솔로만 봤다는 거예요. 좋아요 1240개를 받아냈는데, 정작 화면엔 안 보여요.
다음 시간엔 드디어 이 결과를 화면(DOM)에 직접 그려요. JavaScript로 HTML 요소를 찾아서 내용을 바꾸고, 새 요소를 만들어 붙이는 거죠. 데이터를 받는 동안엔 "로딩 중…"을 띄워두고, 데이터가 오면 그걸 지우고 진짜 내용을 그려넣어요.
오늘 만든 loadPostInteractions의 결과가 콘솔이 아니라 화면 위 숫자로 살아나는 순간이에요. (진짜 서버에서 데이터를 받아오는 fetch는 조금 더 뒤에 배워요. 먼저 화면을 그리는 법부터요.) 멈춰 있던 화면에 생명을 불어넣는 그 첫걸음, 다음 시간에 만나요!
과제
오늘 배운 Promise.all·allSettled·race·any와 async/await를 직접 익혀볼 차례예요. 모든 과제는 콘솔(console.log)에서 확인하고, 오늘 배운 내용만으로 충분히 풀 수 있어요. 진짜 서버 통신(fetch)이나 화면 그리기(DOM)는 아직 쓰지 않아요. async-advanced.js의 uploadPhoto 같은 가짜 Promise를 만들어 활용하세요.
[구현] Promise.all로 세 장 동시 업로드하기
Promise.all로 여러 비동기를 동시에 다뤄보세요.
setTimeout으로 "사진 한 장 업로드"를 흉내 내는 Promise를 돌려주는 함수를 만드세요(이름과 걸리는 시간을 받게).- 그 함수로 사진 세 장(걸리는 시간은 각자 다르게)을
Promise.all로 묶어 동시에 업로드하세요. - 셋 다 끝나면
then에서"모든 사진 전송 완료"와 결과 배열을 찍으세요. - 전체가 끝나는 데 걸린 시간이, 세 장 시간의 합이 아니라 가장 느린 한 장의 시간과 비슷한지 확인해보세요. 왜 그런지 한 문장으로 설명해보세요.
[구현] all과 allSettled로 같은 데이터를 처리해 결과 비교하기
같은 세 개(1개는 실패하게)를 두 방법으로 처리해 차이를 직접 보세요.
- 세 개의 업로드 Promise를 만들되, 가운데 하나는 일부러 실패(
reject)하게 하세요. - 먼저
Promise.all로 묶어then·catch로 처리해보세요. 어느 쪽이 불리는지, 성공한 나머지 결과는 받을 수 있는지 확인하세요. - 같은 세 개를
Promise.allSettled로도 처리해보세요. 각 결과의status·value·reason을 찍어, 실패한 하나를 제외한 나머지가 챙겨지는지 확인하세요. all과allSettled중 "10장 중 1장이 실패해도 나머지 9장을 살리고 싶을 때" 무엇을 써야 할지 한 문장으로 정리해보세요.
[탐구] then+catch vs async/await+try-catch 에러 흐름 비교하기
같은 실패를 두 방식으로 처리하며 에러가 어디로 흐르는지 추적해보세요.
- 일부러 실패(
reject)하는 업로드 Promise를 하나 만드세요. - 먼저
then·catch·finally체이닝으로 처리하며, 실패했을 때then을 건너뛰고catch로 가는지,finally는 항상 불리는지 확인하세요. - 같은 Promise를 async/await +
try-catch-finally로 처리하세요. 실패가catch블록으로 튀는지,try안의 그다음 줄은 건너뛰어지는지 확인하세요. - 두 방식의 출력이 같은지 비교하고,
.then/.catch/.finally와try/catch/finally가 어떻게 1:1로 대응되는지 표로 정리해보세요.
생각해볼 주제
정답을 적는 문제가 아니에요. 오늘 배운 것의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.
1. all이 아니라 allSettled를 써야 하는 순간은 언제일까?
오늘 Promise.all은 하나라도 실패하면 즉시 전체가 실패한다고 했어요. 그런데 사진 10장을 한꺼번에 올리는데 그중 1장만 네트워크 문제로 실패했다고 상상해보세요. all을 쓰면 잘 올라간 9장까지 전부 실패로 묻혀버려요. allSettled라면 9장은 성공으로 챙기고 1장만 따로 "실패했어요"라고 알려줄 수 있죠.
이때 사용자에게 "전부 실패했습니다"라고 하는 것과 "9장은 올라갔고 1장만 다시 시도해주세요"라고 하는 것 중, 어느 쪽이 좋은 경험일까요? "전부 성공이 필요한 일"과 "되는 것만이라도 챙기는 게 나은 일"을 어떻게 구분할지 곱씹어보세요.
2. 순차와 병렬, 무엇을 골라야 할까?
Step 8에서 순차는 약 3초, 병렬은 약 1초였어요. 시간만 보면 병렬이 무조건 좋아 보이는데, 항상 병렬을 쓸 수 있는 건 아니었죠. "유저 정보를 먼저 받고, 그 유저의 ID로 게시물을 조회한다"면 앞 결과가 있어야 다음을 시작할 수 있으니 순차밖에 못 써요. 반대로 "좋아요·댓글·공유처럼 서로 상관없는 데이터 3개"는 동시에 받으면 시간을 아끼죠.
어떤 일들을 마주했을 때 "이건 순차로 가야 하나, 병렬로 묶을 수 있나"를 가르는 기준은 무엇일까요? "서로 의존하는가"라는 한 가지 질문으로 어떻게 판단할 수 있는지 생각해보세요.
✅ 예시 답안정답 보기
🎯 [과제 1 예시답안] Promise.all로 세 장 동시 업로드하기
수업에서 Promise.all로 여러 사진을 한꺼번에 올렸죠. 이 과제는 그 동작을 직접 짜보고, 전체 시간이 왜 "합"이 아니라 "가장 느린 한 장"과 비슷한지 눈으로 확인하는 거예요.
핵심 접근
setTimeout으로 "사진 한 장 업로드"를 흉내 내는 Promise를 돌려주는 함수를 먼저 만들어요. 그 함수로 시간이 각자 다른 세 장을 Promise.all에 배열로 넘기면, 셋이 동시에 출발해요. then은 셋 다 끝났을 때 결과를 배열로 받아요.
예시 구현
function uploadPhoto(name, delay) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`${name} 업로드 완료`);
}, delay);
});
}
console.time("전체");
Promise.all([
uploadPhoto("photo1.jpg", 300),
uploadPhoto("photo2.jpg", 500),
uploadPhoto("photo3.jpg", 400),
])
.then((results) => {
console.log("모든 사진 전송 완료");
console.log(results);
console.timeEnd("전체"); // 약 500ms
});
출력은 이래요.
모든 사진 전송 완료
[ 'photo1.jpg 업로드 완료', 'photo2.jpg 업로드 완료', 'photo3.jpg 업로드 완료' ]
전체: 약 500ms
300·500·400을 더하면 1200ms인데, 실제로는 약 500ms예요. 세 장을 한 줄로 줄 세워 차례차례 올린 게 아니라 동시에 출발시켰기 때문이에요. 셋이 같이 달리면 전체가 끝나는 시점은 "가장 느린 한 장"(500ms)이 끝나는 순간이에요. 결과 배열 순서는 끝난 순서가 아니라 넣은 순서 그대로라는 점도 확인해두면 좋아요.
채점 포인트
| 포인트 | 무엇을 보는가 | 배점 가중 |
|---|---|---|
| 가짜 Promise 함수 | setTimeout으로 delay 뒤 resolve하는 함수를 만들었는가 |
상 |
| 세 장 다른 시간 | 세 장의 delay를 서로 다르게 줬는가 |
중 |
Promise.all 배열 |
세 Promise를 배열로 묶어 Promise.all에 넘겼는가 |
상 |
| then 결과 | then에서 "모든 사진 전송 완료" + 결과 배열을 찍었는가 |
중 |
| 시간 이유 설명 | 전체 시간이 "가장 느린 한 장"과 비슷한 이유를 "동시에 출발해서"로 설명했는가 | 상 |
흔한 실수
Promise.all에 배열이 아니라 Promise를 콤마로 나열함(Promise.all(p1, p2, p3)) →Promise.all은 인자를 하나의 배열로 받아요. 대괄호[ ]로 묶어야 해요.uploadPhoto("photo1.jpg", 300)을.then안에서 하나씩 호출해 이어 붙임 → 그러면 순차 처리라 1200ms가 걸려요. 세 호출을 배열 안에 한꺼번에 적어야 동시에 출발해요.- 전체 시간이 1200ms로 나온다고 적음 → 동시 출발이 안 된 거예요. 배열로 한 번에 넘겼는지 확인하세요.
실무 한 걸음 더
이 패턴이 프로필 화면 한 장을 채울 때 그대로 쓰여요. 사용자 정보·게시물 목록·팔로워 수처럼 서로 상관없는 데이터 여러 개를 Promise.all로 묶으면, 셋을 따로따로 기다릴 때보다 화면이 훨씬 빨리 떠요. 다만 실제 서버 통신에선 한 장이 실패하면 all이 전체를 실패로 처리하는데, "되는 것만이라도 보여주고 싶을 때"가 바로 다음 과제의 allSettled예요.
🎯 [과제 2 예시답안] all과 allSettled로 같은 데이터를 처리해 결과 비교하기
같은 세 개(가운데 하나는 실패)를 Promise.all과 Promise.allSettled 두 방법으로 처리해, "하나 실패하면 어떻게 되나"의 차이를 직접 보는 과제예요.
핵심 접근
uploadPhoto에 성공/실패를 고르는 인자를 하나 더 둬서, 가운데 한 장만 reject하게 만들어요. all은 그 실패 하나 때문에 catch로 빠지고 나머지를 못 받아요. allSettled는 성공·실패를 가리지 않고 끝까지 기다려, 각 결과의 status로 갈라 처리해요.
예시 구현
function uploadPhoto(name, delay, success = true) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve(`${name} 업로드 완료`);
} else {
reject(`${name} 업로드 실패`);
}
}, delay);
});
}
// 방법 1: Promise.all — 가운데 실패 하나에 전체가 무너짐
Promise.all([
uploadPhoto("a.jpg", 300),
uploadPhoto("b.jpg", 400, false), // 실패
uploadPhoto("c.jpg", 500),
])
.then((results) => console.log("[all] 전부 성공:", results))
.catch((error) => console.log("[all] 하나라도 실패:", error));
// 방법 2: Promise.allSettled — 실패해도 나머지를 챙김
Promise.allSettled([
uploadPhoto("a.jpg", 300),
uploadPhoto("b.jpg", 400, false), // 실패
uploadPhoto("c.jpg", 500),
]).then((results) => {
results.forEach((r) => {
if (r.status === "fulfilled") {
console.log("[allSettled] 성공:", r.value);
} else {
console.log("[allSettled] 실패:", r.reason);
}
});
});
출력은 이래요.
[all] 하나라도 실패: b.jpg 업로드 실패
[allSettled] 성공: a.jpg 업로드 완료
[allSettled] 실패: b.jpg 업로드 실패
[allSettled] 성공: c.jpg 업로드 완료
all은 b.jpg 하나가 실패하자마자 catch로 빠져서, 잘 올라간 a.jpg·c.jpg의 결과는 받지 못해요. 반면 allSettled는 셋 다 끝까지 기다린 뒤, 성공은 value로 실패는 reason으로 갈라서 전부 챙겨줘요. "10장 중 1장이 실패해도 나머지 9장을 살리고 싶을 때"는 allSettled예요.
채점 포인트
| 포인트 | 무엇을 보는가 | 배점 가중 |
|---|---|---|
| 가운데 실패 | 세 개 중 가운데 하나를 reject하게 만들었는가 |
상 |
| all의 catch | all에서 then이 아니라 catch가 불리는 걸 확인했는가 |
상 |
| all의 손실 | all이 성공한 나머지 결과를 못 받는다는 걸 짚었는가 |
중 |
| allSettled status | status로 갈라 value/reason을 꺼냈는가 |
상 |
| 결론 정리 | "9장 살리기 = allSettled"로 한 문장 정리했는가 | 상 |
흔한 실수
allSettled결과를 바로r.value로 꺼냄 → 실패한 항목엔value가 없고reason이 있어요.r.status로 먼저 갈라야 해요.all에.catch를 안 붙임 → 가운데가 실패하면 잡히지 않은 거부(unhandled rejection)가 콘솔에 빨갛게 떠요.all을 쓸 땐.catch가 거의 필수예요.allSettled도 하나 실패하면 전체가catch로 갈 거라 예상함 →allSettled는 거의catch로 안 가요. 실패까지 결과 배열에 담아then으로 줘요.
실무 한 걸음 더
"전부 성공해야만 의미 있는 일"과 "되는 것만이라도 챙기는 게 나은 일"을 가르는 게 all과 allSettled 선택의 핵심이에요. 예를 들어 결제는 "장바구니 5개 중 4개만 결제" 같은 게 말이 안 되니 all이 맞고, 사진 여러 장 올리기나 추천 목록 여러 곳에서 모으기는 일부 실패를 따로 안내하는 allSettled가 사용자 경험에 더 좋아요. 이 판단은 바로 뒤 생각해볼 주제 1에서 더 깊게 다뤄요.
🎯 [과제 3 예시답안] then+catch vs async/await+try-catch 에러 흐름 비교하기
같은 실패 하나를 then·catch·finally 체이닝과 async/await + try-catch-finally 두 방식으로 처리해, 에러가 어디로 흐르는지 추적하는 탐구예요. 출력이 똑같고 모양만 다르다는 걸 확인하면 성공이에요.
핵심 접근
일부러 실패하는 업로드 Promise를 하나 만들어요. 한쪽은 .then().catch().finally()로 줄줄이 잇고, 다른 쪽은 같은 Promise를 try 안에서 await하고 catch·finally로 감싸요. 두 출력을 나란히 두면 .then→try, .catch→catch, .finally→finally가 1:1로 맞아떨어져요.
예시 구현
function uploadPhoto(name, delay, success = true) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) resolve(`${name} 업로드 완료`);
else reject(`${name} 업로드 실패`);
}, delay);
});
}
// 방법 1: then / catch / finally 체이닝
uploadPhoto("중요사진", 300, false)
.then((result) => console.log("[then] 성공:", result))
.catch((error) => console.log("[catch] 에러 잡음:", error))
.finally(() => console.log("[finally] 로딩 표시 끄기"));
// 방법 2: async/await + try / catch / finally
async function run() {
try {
const result = await uploadPhoto("중요사진", 300, false);
console.log("[try] 성공:", result); // 실패하면 이 줄은 건너뜀
} catch (error) {
console.log("[catch] 에러 잡음:", error);
} finally {
console.log("[finally] 로딩 표시 끄기");
}
}
run();
두 방식 모두 출력이 같아요.
[catch] 에러 잡음: 중요사진 업로드 실패
[finally] 로딩 표시 끄기
방법 1에선 실패가 .then을 건너뛰고 .catch로 갔어요. 방법 2에선 await가 실패하면서 try 안의 그다음 줄([try] 성공)을 건너뛰고 catch 블록으로 튀었죠. finally는 둘 다 성공이든 실패든 마지막에 항상 불려요. 두 방식은 모양만 다르고 하는 일이 똑같아요.
1:1 대응을 표로 정리하면 이래요.
| then 체이닝 | async/await | 하는 일 |
|---|---|---|
.then(result => ...) |
try { const result = await ... } |
성공 결과 처리 |
.catch(error => ...) |
catch (error) { ... } |
실패(에러) 처리 |
.finally(() => ...) |
finally { ... } |
성공·실패 상관없이 마지막 처리 |
채점 포인트
| 포인트 | 무엇을 보는가 | 배점 가중 |
|---|---|---|
| 실패 Promise | 일부러 reject하는 Promise를 만들었는가 |
중 |
| 체이닝 흐름 | 실패가 .then 건너뛰고 .catch로, .finally는 항상 불리는 걸 확인했는가 |
상 |
| await 흐름 | 실패가 catch로 튀고 try의 그다음 줄을 건너뛰는 걸 확인했는가 |
상 |
| 출력 동일 | 두 방식 출력이 같은지 비교했는가 | 중 |
| 1:1 대응표 | .then→try/.catch→catch/.finally→finally를 표로 정리했는가 |
상 |
흔한 실수
- async/await에서
await없이 호출함(const result = uploadPhoto(...)) → 그러면result에 결과가 아니라 Promise 객체가 담겨, 실패해도catch로 안 가요. 기다릴 곳엔 꼭await를 붙여요. try안에await를 안 두고, 일반 Promise를 그냥try로 감쌈 →try-catch는await한 실패만 잡아요.await없는 Promise의reject는try-catch가 못 잡아요.await를 쓰는 함수에async를 안 붙임 →await는async함수 안에서만 쓸 수 있어요. 함수 앞에async가 있는지 확인하세요.
실무 한 걸음 더
출력이 같다면 어느 쪽을 쓸까요? 단계가 하나면 둘 다 무난하지만, "A 받고 → 그걸로 B 받고 → 그걸로 C 받고" 처럼 단계가 길어질수록 async/await가 위에서 아래로 읽혀 훨씬 따라가기 쉬워요. 에러 처리도 try-catch가 동기 코드와 똑같은 모양이라 익숙하고요.
그래서 요즘은 async/await를 기본으로 쓰고, Promise.all처럼 "여러 개를 한 번에 묶는" 자리에서만 Promise 메서드를 섞어 쓰는 게 흔한 방식이에요.
💭 [생각해볼 주제 예시답안]
1. all이 아니라 allSettled를 써야 하는 순간은 언제일까?
문제 상황 요약
Promise.all은 하나라도 실패하면 즉시 전체가 실패해요. 사진 10장을 한꺼번에 올리는데 1장만 네트워크 문제로 실패하면, all은 잘 올라간 9장까지 전부 실패로 묻어버리죠. 이때 사용자에게 "전부 실패했습니다"와 "9장은 올라갔고 1장만 다시 시도해주세요" 중 어느 쪽이 좋은 경험일까요?
튜터의 가이드 및 해설
핵심은 "이 일이 전부 성공해야만 의미가 있는가"를 따지는 거예요. 두 갈래로 나눠 보면 판단이 쉬워져요.
먼저 전부 성공이 필요한 일이에요. 계좌 이체를 떠올려보세요. "출금은 됐는데 입금은 안 됨" 같은 절반의 성공은 사고예요. 이런 일은 하나라도 실패하면 전체를 없던 일로 돌려야 하니 all이 맞아요. 하나 실패하면 즉시 멈추고 catch로 빠지는 all의 성격이 오히려 안전장치가 되죠.
반대로 되는 것만이라도 챙기는 게 나은 일이에요. 사진 10장 올리기가 딱 그래요. 9장이 잘 올라갔는데 1장 때문에 전부 실패 처리하면, 사용자는 멀쩡히 올라간 9장까지 다시 올려야 해요. 화나는 경험이죠. 이럴 땐 allSettled로 9장은 성공으로 보여주고, 실패한 1장만 "다시 시도" 버튼으로 따로 안내하는 게 훨씬 나아요.
Promise.all: 하나 실패 = 전체 실패. 전부 성공이 한 묶음으로 의미 있을 때(이체, 결제처럼). 빠르게 실패를 알려주는 게 장점, 일부 성공을 버리는 게 단점.Promise.allSettled: 끝까지 기다려 성공·실패를 따로 챙김. 일부 실패를 허용하고 부분 성공을 살리고 싶을 때. 사용자 경험이 부드러운 게 장점, 결과를 일일이status로 갈라 처리해야 하는 게 단점.- 실무에서는 보통: "한 개라도 실패하면 전체가 무의미한가?"를 먼저 물어요. 그렇다면
all, 아니면allSettled. 사용자에게 보여주는 화면용 데이터는 부분 성공이라도 살리는allSettled가 경험상 더 좋은 선택일 때가 많아요.
🎯 면접관을 홀리는 핵심 멘트
"
all과allSettled의 선택은 '이 일이 전부 성공해야만 의미가 있는가'로 갈려요. 이체나 결제처럼 절반의 성공이 사고가 되는 일은 하나 실패하면 전체를 멈추는all이 맞아요. 반대로 사진 10장 올리기처럼 9장이라도 살리는 게 나은 일은allSettled로 부분 성공을 챙기고 실패한 것만 따로 재시도를 안내하는 게 훨씬 좋은 사용자 경험이에요."
2. 순차와 병렬, 무엇을 골라야 할까?
문제 상황 요약
순차는 약 3초, 병렬은 약 1초였어요. 시간만 보면 병렬이 무조건 좋아 보이는데, 항상 병렬을 쓸 수 있는 건 아니었죠. "유저 정보를 먼저 받고, 그 ID로 게시물을 조회한다"면 순차밖에 못 쓰고, "좋아요·댓글·공유" 같은 상관없는 데이터는 병렬로 묶을 수 있어요. 둘을 가르는 기준은 무엇일까요?
튜터의 가이드 및 해설
가르는 질문은 딱 하나예요. "뒤 작업이 앞 결과를 필요로 하는가?" 이 한 문장으로 거의 다 결정돼요.
앞 결과가 있어야 다음을 시작할 수 있으면 순차예요. "유저 정보를 받아서 → 그 유저의 ID로 → 게시물을 조회"가 그래요. 유저 ID를 모르면 게시물 요청 자체를 만들 수 없으니, 앞이 끝날 때까지 기다리는 수밖에 없어요. 이건 코드를 잘못 짠 게 아니라 일의 성격이 그런 거예요. await를 줄줄이 쓰면 자연스럽게 순차가 돼요.
서로 상관없으면 병렬이에요. 한 게시물의 좋아요 수·댓글 목록·공유 수는 서로 알 필요가 없어요. 좋아요를 받으려고 댓글을 기다릴 이유가 없죠. 이런 건 Promise.all로 묶어 동시에 출발시키면, 셋을 따로 기다릴 때보다 시간이 확 줄어요(3초 → 1초).
흔한 함정은 "병렬이 더 빠르니까 무조건 병렬"이라고 생각하는 거예요. 의존 관계가 있는 일을 억지로 병렬로 묶으면, 앞 결과가 없는 상태에서 뒤 요청을 보내게 돼 아예 동작을 안 하거나 틀린 값을 받아요. 빠른 게 문제가 아니라 틀린 거죠.
- 순차(
await연달아): 뒤가 앞 결과에 의존할 때. 안전하지만 시간이 합산돼요(3개면 ≈3초). - 병렬(
Promise.all+await): 서로 의존하지 않을 때. 가장 느린 하나의 시간만 걸려요(3개여도 ≈1초). 단, 의존 관계가 있으면 못 써요. - 실무에서는 보통: 먼저 "이 요청들이 서로 필요로 하나?"를 그려봐요. 의존 사슬은 순차로, 독립적인 묶음은
Promise.all로. 실제 화면은 보통 둘이 섞여 있어서(유저를 먼저 받고, 그다음 좋아요·댓글·공유를 한꺼번에) 순차와 병렬을 함께 쓰게 돼요.
🎯 면접관을 홀리는 핵심 멘트
"순차와 병렬은 '뒤 작업이 앞 결과를 필요로 하는가' 한 가지 질문으로 갈려요. 유저 정보를 받아 그 ID로 게시물을 조회하는 건 의존 관계라 순차밖에 못 쓰고, 좋아요·댓글·공유처럼 서로 상관없는 데이터는
Promise.all로 묶어 병렬로 동시에 받아요. 병렬이 더 빠르다고 의존 관계가 있는 일까지 묶으면, 빠른 게 아니라 틀린 값을 받게 됩니다."