자바스크립트의 비밀스러운 삶: Promise
Source: Dev.to
Timothy는 칠판 앞에 서서 손이 경직되었다. 그는 10분 동안 코드를 작성했지만, 문자 그대로 구석에 끼어 있었다. 그의 코드는 칠판 오른쪽 끝까지 밀려 나무 프레임에 글자를 짜넣고 있었다.
“나는 세 가지 간단한 일을 하려고 하는데,” Timothy가 불평했다. “로그인, 사용자 데이터 가져오기, 그리고 그들의 게시물 가져오기. 그런데 코드가 어디에도 도착하지 못하는 계단처럼 보여.”
그는 자신의 괴물을 보여주기 위해 물러섰다:
login(user, function(token) {
getUser(token, function(profile) {
getPosts(profile.id, function(posts) {
console.log("Finally got posts:", posts);
}, function(err) {
console.error("Error getting posts");
});
}, function(err) {
console.error("Error getting user");
});
}, function(err) {
console.error("Error logging in");
});
Margaret는 새 지우개를 들고 다가왔다. “아,” 그녀가 말했다. “재앙 피라미드야.”
“읽기 힘들어요,” Timothy가 말했다. “괄호 때문에 길을 잃어요. 오류 처리는 악몽이에요.”
Margaret는 삼각형 모양의 코드를 모두 지워버렸다. “그것은 당신이 콜백에 의존하고 있기 때문이야. 라이브러리의 제어권을 다른 사람에게 넘겨주고 그들이 다시 호출해 주길 바라는 거지.”
그녀는 칠판에 하나의 깔끔한 상자를 그렸다.
“이제 Promise에 대해 배울 시간이다.”
The IOU (The Promise Object)
“Promise,” Margaret가 설명했다, “는 객체야. 값 자체가 아니야. 자리표시자—IOU야.”
Pending: “작업 중이야.”
Fulfilled: “데이터가 여기 있어.”
Rejected: “뭔가 잘못됐어.”
“login에 함수를 입력하는 대신, login 함수는 출력으로 Promise 객체를 반환해. 그 객체를 보관하거나, 다른 곳에 넘기고, 명령을 붙일 수 있어.”
const loginPromise = login(user);
loginPromise.then(function(token) {
// This runs only when the promise is fulfilled
return getUser(token);
});
“좋아,” Timothy가 인정했다. “하지만 단계가 세 개라면 여전히 중첩돼야 하지 않을까?”
“아니야,” Margaret가 말했다. “Promise는 또 다른 Promise를 반환하니까. 평평하게 체인할 수 있어.”
login(user)
.then(token => getUser(token))
.then(profile => getPosts(profile.id))
.then(posts => console.log("Finally got posts:", posts))
.catch(err => console.error("Something went wrong:", err));
Timothy가 코드를 따라 읽었다. “평평하고, 마지막에 .catch()가 하나만 있네?”
“맞아,” Margaret가 고개를 끄덕였다. “각 단계마다 오류를 처리하는 대신, 맨 아래에서 한 번에 모두 잡아. 피라미드 구조는 사라졌어.”
일시 정지 (Async / Await)
Timothy는 체인을 바라보았다. “더 깔끔하네,” 그가 동의했다. “하지만 여전히… 달라 보여. 일반적인 위‑에서‑아래 프로그래밍처럼 보이지 않아.”
“맞아,” Margaret가 미소 지었다. “이 체인은 아직 약간 ‘구문적’이야. 진짜 인간처럼 보이는 코드를 원한다면 async / await가 필요해.”
그녀는 다시 칠판을 닦았다.
“event loop 기억나? 스택을 차단할 수 없다고 했던 거 기억해?”
“네,” Timothy가 외웠다. “스택을 일시 정지하면 브라우저가 멈춰요.”
“맞아. 하지만 await는 함수를 일시 정지시키면서 스택을 차단하지 않아.”
async function showUserPosts() {
try {
const token = await login(user);
const profile = await getUser(token);
const posts = await getPosts(profile.id);
console.log(posts);
} catch (error) {
console.error("Something went wrong:", error);
}
}
Timothy는 코드를 바라보며 말했다. “3번째 줄: await login(user). 코드가 거기서 멈추는 거야? 네트워크를 기다리는 거야?”
“함수가 일시 정지되는 거야,” Margaret가 바로 잡았다. “엔진이 await를 만나면 이 함수를 일시 중단하고 사실상 옆으로 물러나는 거야. 브라우저의 나머지 부분에 제어권을 반환하지.”
그녀는 스택의 간단한 다이어그램을 그렸다.
“이 함수가 일시 정지된 동안, 브라우저는 살아 있어—클릭을 처리하고 스타일을 렌더링하지. 스택은 비워져 있어.”
“데이터가 돌아오면 어떻게 돼?”
“함수의 나머지 부분—연속성—이 microtask queue(VIP 라인)로 들어가. 스택이 비어 있으면 함수가 다시 돌아와서 정확히 4번째 줄에서 중단된 지점부터 데이터를 가지고 재개돼.”
결론
Timothy는 보드에 있는 세 가지 버전을 바라보았다:
- 피라미드 (콜백)
- 체인 (프로미스)
- 스토리 (async/await)
“그들 모두 같은 일을 한다는 것을 티모시가 깨달았다.”
“그렇지,” Margaret가 말했다. “하지만 마지막 것이 진실을 말한다. 그것은 동기식처럼 보이는 코드를 작성하게 해준다—단일 try/catch 블록으로 단계별로—그럼에도 비동기적으로 동작한다.”
그녀는 그에게 분필을 건넸다.
“이제 논리를 중첩할 필요 없어, 티모시. 그냥 기다리기만 하면 돼.”