업데이트:

[01] 비동기 처리를 위한 콜백 패턴의 단점

1) 콜백 헬

// 비동기 함수
function get(url) {
  // XMLHttpRequest 객체 생성
  const xhr = new XMLHttpRequest();

  // 서버 응답 시 호출될 이벤트 핸들러
  xhr.onreadystatechange = function () {
    // 서버 응답 완료가 아니면 무시
    if (xhr.readyState !== XMLHttpRequest.DONE) return;

    if (xhr.status === 200) {
      // 정상 응답
      console.log(xhr.response);
      // 비동기 함수의 결과에 대한 처리는 반환할 수 없다.
      return xhr.response; // ①
    } else {
      // 비정상 응답
      console.log("Error: " + xhr.status);
    }
  };

  // 비동기 방식으로 Request 오픈
  xhr.open("GET", url);
  // Request 전송
  xhr.send();
}

// 비동기 함수 내의 readystatechange 이벤트 핸들러에서 처리 결과를 반환(①)하면 순서가 보장되지 않는다.
const res = get("http://jsonplaceholder.typicode.com/posts/1");
console.log(res); // ② undefined

위 코드를 통해 간단히 콜백 헬이라는 개념에 대해 요약해보겠다.

먼저 크게 console.log 실행 부분과 get 비동기 함수로 코드를 나누어서 보자.

res 변수에 url을 인자로 넘겨준 get 함수를 담아주었다. 그리고 나서 console.log(res)로 get 함수의 대한 결과를 확인했더니 undefined가 출력되었다.

그 이유는 get 함수가 비동기 함수라서 console.log 문이 종료되어 콜 스택이 비어야 get 함수의 이벤트 핸들러가 실행되기 때문이다.

결국, get 함수의 처리 결과를 가지고 후속 처리를 할 수 없는 “콜백 헬” 현상이 일어난 것이다.

2) 에러 처리의 한계

try {
  // throw 연산자 : 에러를 생성함
  setTimeout(() => {
    throw new Error("Error!");
  }, 1000);
} catch (e) {
  console.log("에러를 캐치하지 못한다..");
  console.log(e);
}

try 블록 내에서 setTimeout 함수가 실행되면 1초 후에 콜백 함수 안의 문이 실행된다.

원래는 콜백 함수에서 에러가 있으므로 catch 블록의 코드가 실행되어야 하지만 에러를 캐치하지 못한다.

그 이유는 setTimeout 함수가 비동기 함수이므로 콜백 함수가 실행될 때까지 기다리지 않고 즉시 종료되어 호출 스택에서 제거되기 때문이다.

이러한 문제를 극복하기 위해 ES6에서 Promise가 도입되었다.

[02] 프로미스의 생성

Promise 생성자 함수를 new 연산자와 함께 호출하면 프로미스 객체를 생성한다.

프로미스 객체는 String, Number와 같은 표준 빌트인 객체이다.

Promise 생성자 함수는 비동기 작업을 수행할 콜백 함수를 resolve와 reject 함수를 인자로 전달받는다.

여기서 resolve 와 reject 함수는 자바스크립트에서 자체 제공하는 콜백함수이다.

// Promise 객체의 생성
const promise = new Promise((resolve, reject) => {
	if (/*비동기 작업 수행 성공 */) {
		resolve('result');
	}
	else { /* 비동기 작업 수행 실패 */
		reject('failure reason');
	}
});

Promise는 비동기 처리가 성공(fulfilled)하였는지 또는 실패(rejected)하였는지 등의 상태(state) 정보를 갖는다.

상태 의미 구현
pending 비동기 처리가 아직 수행되지 않은 상태 resolve 또는 reject 함수가 아직 호출되지 않은 상태
fulfilled 비동기 처리가 수행된 상태 (성공) resolve 함수가 호출된 상태
rejected 비동기 처리가 수행된 상태 (실패) reject 함수가 호출된 상태
settled 비동기 처리가 수행된 상태 (성공 또는 실패) resolve 또는 reject 함수가 호출된 상태

보통 처음에는 “pending”(보류) 였다가 resolve가 호출되면 value로, reject가 호출되면 error로 변한다.

아래는 Promise의 상태와 결과값이 둘 중 하나로 변화된다는 것을 나타낸 그림이다.

Untitled)

// fulfilled promise(약속이 이행된 프라미스)
let promise = new Promise(function (resolve, reject) {
  // 프라미스가 만들어지면 executor 함수가 자동으로 실행

  // 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 '완료'가 된다.
  setTimeout(() => resolve("완료"), 1000);
});

// 약속한 작업을 거부하는 경우
let promise = new Promise(function (resolve, reject) {
  // 1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보낸다.
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

프로미스의 상태와 결과값은 위 코드에서 resolve가 호출되면 그림에서 파란색 박스가 될 것이고 reject가 호출되면 빨간색 박스가 되는 것이다.

그리고 파란색 박스와 빨간색 박스 모두 프로미스 객체의 상태가 변화했으므로 처리된(settled) 프로미스라고 부른다. 반대로 왼쪽의 검은색 박스는 대기(pending) 상태의 프로미스이다.

결국, 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체라고 정리할 수 있다.

[03] 프로미스의 후속 처리 메서드

프로미스의 비동기 처리 상태가 변화하면 이에 따른 후속 처리가 일어나는데 이를 위해 프로미스는 후속 메서드 then, catch, finally를 제공한다.

그리고 이 메서드들은 언제나 프로미스를 반환한다.

1) then

then 메서드는 두 개의 콜백 함수를 인수로 전달받는다.

  • 첫 번째 콜백 함수(성공 처리) : 프로미스가 fulfilled 상태가 되면 호출, result를 인수로 전달받음

  • 두 번째 콜백 함수(실패 처리) : 프로미스가 refected 상태가 되면 호출, 에러를 인수로 전달받음

new Promise((resolve) => resolve("fulfilled")).then(
  (v) => console.log(v),
  (e) => console.error(e)
); // fulfilled

new Promise((_, reject) => reject(new Error("rejected"))).then(
  (v) => console.log(v),
  (e) => console.error(e)
); // Error: rejected

2) catch

catch 메서드는 한 개의 콜백 함수를 인수로 전달받는다.

프로미스가 rejected 상태인 경우만 호출된다.

new Promise((_, reject) => reject(new Error("rejected"))).catch((e) =>
  console.log(e)
); // Error: rejected

3) finally

finally 메서드는 한 개의 콜백 함수를 인수로 전달받는다.

프로미스의 성공/실패와 상관없이 무조건 한 번 호출된다.

프로미스의 상태와 상관없이 공통적으로 수행해야 할 처리 내용이 있을 때 유용하다.

new Promise(() => {}).finally(() => console.log("finally")); // finally

[04] 프로미스의 에러 처리

비동기 처리를 위한 콜백 함수의 에러 처리의 한계를 보완하고자 프로미스가 등장했다고 하였다.

그렇다면 프로미스의 에러 처리는 어떻게 이루어질까?

바로 후속 처리 메서드들을 이용해주면 된다.

  1. then 메서드의 두 번째 콜백 함수로 처리
const wrongUrl = "https://jsonplaceholder.typicode.com/XXX/1";

promiseGet(wrongUrl).then(
	res => console.log(res),
	err => console.error(err)
); // Error: 404
  1. catch 메서드로 처리
const wrongUrl = "https://jsonplaceholder.typicode.com/XXX/1";

promiseGet(wrongUrl)
	.then(res => console.log(res))
	.catch(err => console.error(err)); //Error: 404

둘 중 가독성 면에서 catch 메서드를 사용하는 것이 좀 더 좋아보이므로 에러 처리를 할 경우에는 2번째 방법을 이용하자 !

[05] 프로미스 체이닝

catch 메서드로 에러를 처리하는 코드에서 then메서드와 catch메서드가 순서대로 호출되었다.

그런데 프로미스 후속 메소드들은 모두 프로미스를 반환하므로 이를 프로미스 체이닝이라고 한다.

new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000); // (*)
})
  .then(function (result) {
    // (**)

    alert(result); // 1
    return result * 2;
  })
  .then(function (result) {
    // (***)

    alert(result); // 2
    return result * 2;
  })
  .then(function (result) {
    alert(result); // 4
    return result * 2;
  });

[06] 프로미스의 정적 메소드

1) Promise.resolve / reject

Promise.resolve와 Promise.reject 메소드는 존재하는 값을 Promise로 래핑하기 위해 사용한다.

정적 메소드 Promise.resolve 메소드는 인자로 전달된 값을 resolve하는 Promise를 생성한다.

const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [ 1, 2, 3 ]

Promise.reject 메소드는 인자로 전달된 값을 reject하는 프로미스를 생성한다.

const rejectedPromise = Promise.reject(new Error("Error!"));
rejectedPromise.catch(console.log); // Error: Error!

2) Promise.all

Promise.all 메소드는 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받는다.

전달받은 모든 프로미스를 병렬로 처리하고 그 처리 결과를 resolve하는 새로운 프로미스를 반환한다.

Promise.all([
  new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1
  new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2
  new Promise((resolve) => setTimeout(() => resolve(3), 1000)), // 3
])
  .then(console.log) // [ 1, 2, 3 ]
  .catch(console.log);

Promise.all 메소드는 3개의 프로미스를 담은 배열을 전달받았다.

  • 첫번째 프로미스 : 3초 후에 1을 resolve하여 처리 결과를 반환한다.

  • 두번째 프로미스 : 2초 후에 2을 resolve하여 처리 결과를 반환한다.

  • 세번째 프로미스 : 1초 후에 3을 resolve하여 처리 결과를 반환한다.

Promise.all 메소드는 전달받은 모든 프로미스를 병렬로 처리한다. 이때 모든 프로미스의 처리가 종료될 때까지 기다린 후 아래와 모든 처리 결과를 resolve 또는 reject한다.

  • 모든 프로미스의 처리가 성공하면 각각의 프로미스가 resolve한 처리 결과를 배열에 담아 resolve하는 새로운 프로미스를 반환되고 처리 순서가 보장된다.

  • 프로미스의 처리가 하나라도 실패하면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환한다.

Promise.all([
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Error 1!")), 3000)
  ),
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Error 2!")), 2000)
  ),
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Error 3!")), 1000)
  ),
])
  .then(console.log)
  .catch(console.log); // Error: Error 3!

위 예제의 경우, 세번째 프로미스가 1초 후에 실행되어 가장 먼저 실패하므로 세번째 프로미스가 reject한 에러가 catch 메소드로 전달된다.

Promise.all 메소드는 전달 받은 이터러블의 요소가 프로미스가 아닌 경우, Promise.resolve 메소드를 통해 프로미스로 래핑된다.

Promise.all([
  1, // => Promise.resolve(1)
  2, // => Promise.resolve(2)
  3, // => Promise.resolve(3)
])
  .then(console.log) // [1, 2, 3]
  .catch(console.log);

3) Promise.race

Promise.race 메소드는 Promise.all 메소드와 동일하게 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받는다.

그리고 Promise.race 메소드는 Promise.all 메소드처럼 모든 프로미스를 병렬 처리하는 것이 아니라 가장 먼저 처리된 프로미스가 resolve한 처리 결과를 resolve하는 새로운 프로미스를 반환한다.

Promise.race([
  new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1
  new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2
  new Promise((resolve) => setTimeout(() => resolve(3), 1000)), // 3
])
  .then(console.log) // 3
  .catch(console.log);

에러가 발생한 경우는 Promise.all 메소드와 동일하게 처리된다.

Promise.race([
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Error 1!")), 3000)
  ),
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Error 2!")), 2000)
  ),
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Error 3!")), 1000)
  ),
])
  .then(console.log)
  .catch(console.log); // Error: Error 3!

4) Promise.allSetteled

Promise.allSetteled 메소드도 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받는다.

전달받은 프로미스가 모두 settled 상태가 되면 처리 결과를 배열로 반환한다.

Promise.allSettled([
  new Promise((resolve) => setTimeout(() => resolve(1), 2000)),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Error!")), 1000)
  ),
]).then(console.log);
/*
[
	{status: 'fulfilled', value: 1} 
	{status: 'rejected', reason: Error: Error!}
]
*/

[07] 마이크로태스크 큐

이전에 배웠던 콜백 함수는 태스크 큐에 저장된다고 하였다.

그런데 후속 처리 메서드의 콜백 함수는 태스크 큐가 아닌 마이크로태스크 큐에 저장된다.

setTimeout(() => console.log(1), 0);

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));

// 마이크로 태스크 큐가 우선순위가 더 높으므로 2->3->1 순서로 출력된다.

마이크로태스크 큐는 태스크 큐보다 우선순위가 높다는 것이 특징이다.

이벤트 루프가 콜 스택이 비면 마이크로태스크 큐에서 대기하는 함수를 가져와 실행한다.

[08] fetch

fetch 메서드는 서버에 네트워크 요청을 보내고 받아오는 방법으로 자주 사용되는 친구이다.

XMLHttpRequest 객체보다 사용법이 간단하고 프로미스를 지원하기 때문에 콜백 패턴의 단점에서 자유롭다.

🔍 기본 문법

let promise = fetch(url, [options]);
// url: 접근하고자 하는 url
// options : 선택 매개변수, method나 header를 지정할 수 있음(아무것도 넘기지 않으면 GET메서드로 진행)

fetch()를 호출하고 json 메서드를 사용하여 역직렬화를 시켜주면 아래와 같이 출력된다.

fetch("https://jsonplaceholder.typicode.com/todos/1")
  .then((response) => response.json()) // 역직렬화
  .then((json) => console.log(json));

/*
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}
*/

댓글남기기