|
| 1 | +# 45장 프로미스 |
| 2 | + |
| 3 | +자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다. 하지만 전통적인 콜백 패턴은 **콜백 헬(Callback Hell)**로 인해 가독성이 나쁘고, 비동기 처리 중 발생한 에러의 처리가 곤란하며, 여러 개의 비동기 처리를 한 번에 처리하는 데도 한계가 있다. |
| 4 | + |
| 5 | +ES6에서는 비동기 처리를 위한 또 다른 패턴으로 **프로미스(Promise)**를 도입했다. 프로미스는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다. |
| 6 | + |
| 7 | +## 45.1 비동기 처리를 위한 콜백 패턴의 단점 |
| 8 | + |
| 9 | +### 45.1.1 콜백 헬 |
| 10 | + |
| 11 | +비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수도 없다. 따라서 비동기 함수의 처리 결과(성공/실패)에 대한 후속 처리는 비동기 함수 내부에서 수행해야 한다. |
| 12 | + |
| 13 | +이때 후속 처리를 위해 콜백 함수를 전달하는데, 콜백 함수 내에서 다시 비동기 호출을 하면서 콜백 함수 호출이 중첩되어 복잡도가 높아지는 현상을 **콜백 헬**이라 한다. |
| 14 | + |
| 15 | +```javascript |
| 16 | +// 콜백 헬 예시 |
| 17 | +get('/step1', (a) => { |
| 18 | + get(`/step2/${a}`, (b) => { |
| 19 | + get(`/step3/${b}`, (c) => { |
| 20 | + console.log(c); |
| 21 | + }); |
| 22 | + }); |
| 23 | +}); |
| 24 | +``` |
| 25 | + |
| 26 | +### 45.1.2 에러 처리의 한계 |
| 27 | + |
| 28 | +가장 심각한 문제는 **에러 처리가 곤란하다**는 것이다. |
| 29 | + |
| 30 | +```javascript |
| 31 | +try { |
| 32 | + setTimeout(() => { |
| 33 | + throw new Error('Error!'); |
| 34 | + }, 1000); |
| 35 | +} catch (e) { |
| 36 | + // 에러를 캐치하지 못한다. |
| 37 | + console.error('캐치한 에러', e); |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +`setTimeout`의 콜백 함수가 실행될 때 `setTimeout` 함수는 이미 콜 스택에서 제거된 상태다. 에러는 호출자(caller) 방향으로 전파되는데, 콜백 함수의 호출자가 `setTimeout`이 아니기 때문에 `catch` 블록에서 에러를 잡을 수 없다. |
| 42 | + |
| 43 | +## 45.2 프로미스의 생성 |
| 44 | + |
| 45 | +`Promise` 생성자 함수를 `new` 연산자와 함께 호출하면 프로미스 객체를 생성한다. |
| 46 | + |
| 47 | +```javascript |
| 48 | +// 프로미스 생성 |
| 49 | +const promise = new Promise((resolve, reject) => { |
| 50 | + // 비동기 작업 수행 |
| 51 | + if (/* 비동기 처리 성공 */) { |
| 52 | + resolve('result'); |
| 53 | + } else { /* 비동기 처리 실패 */ |
| 54 | + reject('failure reason'); |
| 55 | + } |
| 56 | +}); |
| 57 | +``` |
| 58 | + |
| 59 | +### 프로미스의 상태 정보 |
| 60 | + |
| 61 | +| 상태 정보 | 의미 | 상태 변경 조건 | |
| 62 | +| :------------ | :------------------------------------ | :------------------------------- | |
| 63 | +| **pending** | 비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 기본 상태 | |
| 64 | +| **fulfilled** | 비동기 처리가 수행된 상태 (성공) | `resolve` 함수 호출 | |
| 65 | +| **rejected** | 비동기 처리가 수행된 상태 (실패) | `reject` 함수 호출 | |
| 66 | + |
| 67 | +`fulfilled` 또는 `rejected` 상태를 **settled** 상태라고 한다. 일단 `settled` 상태가 되면 더 이상 다른 상태로 변화할 수 없다. |
| 68 | + |
| 69 | +## 45.3 프로미스의 후속 처리 메서드 |
| 70 | + |
| 71 | +프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드(`then`, `catch`, `finally`)에 인수로 전달한 콜백 함수가 선택적으로 호출된다. **모든 후속 처리 메서드는 프로미스를 반환하며, 비동기로 동작한다.** |
| 72 | + |
| 73 | +### 45.3.1 Promise.prototype.then |
| 74 | + |
| 75 | +두 개의 콜백 함수를 인수로 받는다. |
| 76 | + |
| 77 | +- 첫 번째: `fulfilled` 상태(성공) 시 호출 |
| 78 | +- 두 번째: `rejected` 상태(실패) 시 호출 |
| 79 | + |
| 80 | +```javascript |
| 81 | +new Promise((resolve) => resolve('fulfilled')).then( |
| 82 | + (v) => console.log(v), |
| 83 | + (e) => console.error(e) |
| 84 | +); |
| 85 | +``` |
| 86 | + |
| 87 | +### 45.3.2 Promise.prototype.catch |
| 88 | + |
| 89 | +`rejected` 상태(실패) 시 호출될 콜백 함수를 인수로 받는다. `then(undefined, onRejected)`와 동일하게 동작한다. |
| 90 | + |
| 91 | +### 45.3.3 Promise.prototype.finally |
| 92 | + |
| 93 | +성공/실패와 상관없이 무조건 한 번 호출된다. 공통적인 뒷정리 작업에 유용하다. |
| 94 | + |
| 95 | +## 45.4 프로미스의 에러 처리 |
| 96 | + |
| 97 | +에러 처리는 `then` 메서드의 두 번째 인수보다 **`catch` 메서드를 사용하는 것을 권장**한다. `catch` 메서드를 사용하면 `then` 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있고 가독성도 더 좋다. |
| 98 | + |
| 99 | +```javascript |
| 100 | +promiseGet(url) |
| 101 | + .then((res) => console.log(res)) |
| 102 | + .catch((err) => console.error(err)); // 권장 |
| 103 | +``` |
| 104 | + |
| 105 | +## 45.5 프로미스 체이닝 |
| 106 | + |
| 107 | +`then`, `catch`, `finally` 후속 처리 메서드는 언제나 프로미스를 반환하므로 연속적으로 호출할 수 있다. 이를 **프로미스 체이닝(Promise Chaining)**이라 한다. |
| 108 | + |
| 109 | +```javascript |
| 110 | +promiseGet(`${url}/posts/1`) |
| 111 | + .then(({ userId }) => promiseGet(`${url}/users/${userId}`)) |
| 112 | + .then((userInfo) => console.log(userInfo)) |
| 113 | + .catch((err) => console.error(err)); |
| 114 | +``` |
| 115 | + |
| 116 | +## 45.6 프로미스의 정적 메서드 |
| 117 | + |
| 118 | +### 45.6.1 Promise.resolve / Promise.reject |
| 119 | + |
| 120 | +이미 존재하는 값을 래핑하여 프로미스를 생성하기 위해 사용한다. |
| 121 | + |
| 122 | +- `Promise.resolve`는 인수로 전달받은 값을 `resolve`하는 프로미스를 생성한다. |
| 123 | +- `Promise.reject`는 인수로 전달받은 값을 `reject`하는 프로미스를 생성한다. |
| 124 | + |
| 125 | +### 45.6.2 Promise.all |
| 126 | + |
| 127 | +여러 개의 비동기 처리를 모두 병렬(parallel) 처리할 때 사용한다. 전달받은 모든 프로미스가 `fulfilled` 상태가 되면 모든 결과를 배열에 담아 반환한다. 하나라도 `rejected` 되면 즉시 에러를 반환한다. |
| 128 | + |
| 129 | +### 45.6.3 Promise.race |
| 130 | + |
| 131 | +가장 먼저 `fulfilled` 또는 `rejected` 상태가 된 프로미스의 결과를 반환한다. |
| 132 | + |
| 133 | +### 45.6.4 Promise.allSettled |
| 134 | + |
| 135 | +전달받은 프로미스가 모두 `settled` 상태(성공 또는 실패)가 되면 결과를 반환한다. 실패하더라도 중단되지 않고 모든 결과를 확인할 수 있다. |
| 136 | + |
| 137 | +## 45.7 마이크로태스크 큐 |
| 138 | + |
| 139 | +프로미스의 후속 처리 메서드(`then`, `catch`, `finally`)의 콜백 함수는 **태스크 큐**가 아니라 **마이크로태스크 큐**에 저장된다. |
| 140 | +**마이크로태스크 큐는 태스크 큐보다 우선순위가 높다.** |
| 141 | + |
| 142 | +```javascript |
| 143 | +setTimeout(() => console.log(1), 0); // 태스크 큐 |
| 144 | + |
| 145 | +Promise.resolve() |
| 146 | + .then(() => console.log(2)) // 마이크로태스크 큐 |
| 147 | + .then(() => console.log(3)); // 마이크로태스크 큐 |
| 148 | + |
| 149 | +// 출력 순서: 2 -> 3 -> 1 |
| 150 | +``` |
| 151 | + |
| 152 | +## 45.8 fetch |
| 153 | + |
| 154 | +`fetch` 함수는 `XMLHttpRequest` 객체보다 사용법이 간단하고 프로미스를 지원하는 HTTP 요청 전송 기능의 Web API다. |
| 155 | + |
| 156 | +```javascript |
| 157 | +fetch('https://jsonplaceholder.typicode.com/todos/1') |
| 158 | + .then((response) => response.json()) |
| 159 | + .then((json) => console.log(json)); |
| 160 | +``` |
| 161 | + |
| 162 | +**주의할 점**: `fetch` 함수가 반환하는 프로미스는 **HTTP 에러(404, 500 등)가 발생해도 `reject`하지 않고 `resolve`한다.** (단, `ok` 상태를 `false`로 설정). 오직 네트워크 장애나 요청이 완료되지 못한 경우에만 `reject`한다. |
| 163 | +따라서 `response.ok`를 확인하여 에러 처리를 해야 한다. |
| 164 | + |
| 165 | +```javascript |
| 166 | +fetch(wrongUrl) |
| 167 | + .then((response) => { |
| 168 | + if (!response.ok) throw new Error(response.statusText); |
| 169 | + return response.json(); |
| 170 | + }) |
| 171 | + .catch((err) => console.error(err)); |
| 172 | +``` |
| 173 | + |
| 174 | +## 과제: 퀴즈 |
| 175 | + |
| 176 | +### 퀴즈 1 |
| 177 | + |
| 178 | +프로미스의 3가지 상태(state)는 무엇인가? |
| 179 | + |
| 180 | +<details> |
| 181 | + <summary>정답 및 해설</summary> |
| 182 | + |
| 183 | +**정답:** `pending`(대기), `fulfilled`(이행), `rejected`(거부) |
| 184 | + |
| 185 | +**해설:** |
| 186 | + |
| 187 | +- **pending**: 비동기 처리가 아직 수행되지 않은 기본 상태. |
| 188 | +- **fulfilled**: 비동기 처리가 성공적으로 수행된 상태 (`resolve` 호출 시). |
| 189 | +- **rejected**: 비동기 처리가 실패한 상태 (`reject` 호출 시). |
| 190 | + `fulfilled`와 `rejected`를 합쳐 `settled` 상태라고 부르며, 한 번 `settled` 되면 상태는 변하지 않습니다. |
| 191 | + |
| 192 | +[👉 관련 내용으로 이동: 45.2 프로미스의 생성](#452-프로미스의-생성) |
| 193 | + |
| 194 | +</details> |
| 195 | + |
| 196 | +### 퀴즈 2 |
| 197 | + |
| 198 | +프로미스 체이닝에서 에러 처리를 위해 `then`의 두 번째 인수를 사용하는 것보다 `catch`를 권장하는 이유는? |
| 199 | + |
| 200 | +<details> |
| 201 | + <summary>정답 및 해설</summary> |
| 202 | + |
| 203 | +**정답:** `then` 내부의 에러까지 잡을 수 있고 가독성이 좋기 때문. |
| 204 | + |
| 205 | +**해설:** |
| 206 | +`then`의 두 번째 콜백 함수는 같은 `then`의 첫 번째 콜백 함수에서 발생한 에러를 잡을 수 없습니다. 반면 `catch`는 그 앞의 모든 `then` 체인과 비동기 처리에서 발생한 에러를 모두 잡을 수 있으며 코드 가독성도 더 뛰어납니다. |
| 207 | + |
| 208 | +[👉 관련 내용으로 이동: 45.4 프로미스 에러 처리](#454-프로미스-에러-처리) |
| 209 | + |
| 210 | +</details> |
| 211 | + |
| 212 | +### 퀴즈 3 |
| 213 | + |
| 214 | +`Promise.all`과 `Promise.race`의 차이점은? |
| 215 | + |
| 216 | +<details> |
| 217 | + <summary>정답 및 해설</summary> |
| 218 | + |
| 219 | +**정답:** `all`은 모두 성공해야 완료, `race`는 가장 빠른 하나만 완료되면 종료. |
| 220 | + |
| 221 | +**해설:** |
| 222 | + |
| 223 | +- **Promise.all**: 전달받은 모든 프로미스가 `fulfilled` 될 때까지 기다렸다가 결과 배열을 반환합니다. 하나라도 실패하면 즉시 실패 처리됩니다. |
| 224 | +- **Promise.race**: 전달받은 프로미스 중 가장 먼저 처리된(성공이든 실패든) 프로미스의 결과를 그대로 반환합니다. |
| 225 | + |
| 226 | +[👉 관련 내용으로 이동: 45.6 프로미스의 정적 메서드](#456-프로미스의-정적-메서드) |
| 227 | + |
| 228 | +</details> |
| 229 | + |
| 230 | +### 퀴즈 4 |
| 231 | + |
| 232 | +`fetch` 함수 사용 시 HTTP 404 에러가 발생하면 `catch` 블록이 실행되는가? |
| 233 | + |
| 234 | +<details> |
| 235 | + <summary>정답 및 해설</summary> |
| 236 | + |
| 237 | +**정답:** 실행되지 않는다. (오답 주의!) |
| 238 | + |
| 239 | +**해설:** |
| 240 | +`fetch`는 네트워크 장애 등을 제외한 HTTP 에러(4xx, 5xx)에 대해서는 프로미스를 `reject` 하지 않습니다. 대신 `resolve` 상태의 응답 객체를 반환하며 `ok` 프로퍼티가 `false`가 됩니다. 따라서 `response.ok`를 체크하여 수동으로 에러를 던져야 합니다. |
| 241 | + |
| 242 | +[👉 관련 내용으로 이동: 45.8 fetch](#458-fetch) |
| 243 | + |
| 244 | +</details> |
| 245 | + |
| 246 | +### 퀴즈 5 |
| 247 | + |
| 248 | +마이크로태스크 큐에 들어가는 대표적인 작업은 무엇인가? |
| 249 | + |
| 250 | +<details> |
| 251 | + <summary>정답 및 해설</summary> |
| 252 | + |
| 253 | +**정답:** 프로미스의 후속 처리 메서드(`then`, `catch`, `finally`)의 콜백 함수. |
| 254 | + |
| 255 | +**해설:** |
| 256 | +`setTimeout`, `setInterval` 등의 콜백은 일반 **태스크 큐**에 들어가는 반면, 프로미스의 핸들러는 우선순위가 높은 **마이크로태스크 큐**에 들어갑니다. 따라서 이벤트 루프는 콜 스택이 비면 태스크 큐보다 마이크로태스크 큐를 먼저 비웁니다. |
| 257 | + |
| 258 | +[👉 관련 내용으로 이동: 45.7 마이크로태스크 큐](#457-마이크로태스크-큐) |
| 259 | + |
| 260 | +</details> |
| 261 | + |
| 262 | +## 추천 자료 |
| 263 | + |
| 264 | +- [MDN - Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise) |
| 265 | +- [MDN - Using Promises](https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Using_promises) |
| 266 | +- [Modern JavaScript Deep Dive 45장](http://www.yes24.com/Product/Goods/92742567) |
0 commit comments