해당 포스팅은 위키북스의 모던 자바스크립트 Deep Dive라는 책을 독학하며 기록하는 글입니다.

ES6에서는 비동기 처리를 위한 또 다른 패턴으로 프로미스를 도입했다. 프로미스는 전통적인 콜백 패턴이 가진 단점을 보오나하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있으니 자세히 살펴보자.

전통적인 콜백 패턴

기존의 비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 즉시 종료된다. 즉, 비동기 함수 내부의 비동기로 동작하는 코드는 바깥의 비동기 함수가 종료된 이후에 완료된다. 따라서 내부의 비동기 함수는 코드의 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않는데 이해를 위해 다음의 코드를 보도록 하자.

let a = 0;

setTimeout(() => { a = 1; }, 1000);
console.log(a); // 0

위와 같은 코드에서 개발자는 1초뒤에 변수 a에 1이 할당되고 이후에 출력해서 1이 출력되기를 기대했으나, 실제 실행결과는 a에 1을 할당하기 전에 다음 코드인 출력코드를 실행해서 0이 그대로 출력되고 있다.

위와 같은 문제를 가지고 있는 메서드는 setTimeout 뿐만이 아니라 비동기로 동작하는 setInterval메서드나 이벤트 핸들러들도 마찬가지다. 따라서 우리는 이러한 비동기 함수의 처리 결과를 비동기 함수의 바깥이 아니라 비동기 함수 내부에서 처리를 해줘야 하는데, 전통적으로는 이러한 처리를 위해 비동기 처리를 할 때 그 처리의 결과가 성공일 때 실행할 콜백함수와 실패할 때 실행할 콜백함수를 모두 전달해서 비동기 함수 내부에서 성공 여부에 따라 콜백함수를 선택적으로 실행시켰다.

하지만 만약 첫 번째 비동기 함수에 대한 처리를 다시 비동기 함수가 진행하게 된다면 어떻게 될까? 콜백함수를 통한 비동기 처리 결과에 대한 후속 처리를 수행하는 코드가 다시 콜백함수 방식으로 결과를 처리하고 다시 비동기 방식으로 코드를 기다리고 하면 결국 콜백함수의 호출이 중첩되어 복잡도가 높아지는데 이를 콜백 헬이라고 한다.

콜백 헬 현상이 일어나는 코드에서 가장 문제가 되는 점은 바로 에러 처리이다. 비동기 처리를 통해 내부에서 후속 처리를 해결하는 코드는 사실 자바스크립트 엔진 입장에서는 맨 위의 비동기 함수를 실행만 시키고 바로 다음 코드로 넘어가면 되기 때문에 그 내부에서 일어나는 오류에 대해서는 잡아내지 못한다. 다음 코드를 보자.

try {
  setTimeout(() => { throw new Error("만들어낸 오류"); }, 1000);
} catch(err) {
  console.log("오류가 발생했습니다!");
}

위 코드의 catch에서는 오류를 잡아내지 못한다. setTimeout 메서드는 1초 뒤 에러를 고의적으로 만들어 발생시키지만 이미 setTimeout 메서드를 실행시키는 시점에서 에러가 발생하지 않았기 때문에 catch문은 실행되지 않고 그 다음 코드로 넘어가버렸기 때문이다.

프로미스 객체

위와 같은 콜백 헬, 비동기 함수에서의 에러 처리 등의 문제를 해결하기 위해 ES6에서는 프로미스라는 객체를 도입했다. 프로미스에 대해 간단하게 설명을 하면 비동기 처리 결과에 대한 상태값을 가지고 그 상태값에 대해 각기 다른 처리를 할 수 있는 객체이다. 먼저 프로미스 객체를 생성하기 위해서는 new 키워드와 Promise 생성자 함수를 사용하고 비동기 처리를 진행할 콜백함수를 인수로 전달받는다. 해당 콜백함수는 내부에서 비동기 처리가 성공했을 때 실행할 함수와 실패했을 때 실행할 함수를 전달받는다.

const promise = new Promise((resolve, reject) => {
  // 이 곳에서 비동기 처리에 대한 코드를 진핸한다.
  
  if(/* 비동기 처리 성공시 */) {
    // 프로미스 객체의 상태를 fulfiiled 상태로 바꾸며 인수로 들어온 값을 value로 갖게한다.
    resolve();
  } else {
    // 비동기 처리 실패시
    // 프로미스 객체의 상태를 rejected 상태로 바꾸며 인수로 들어온 Error 객체를 value로 갖게한다.
    reject();
  }
})

그럼 프로미스 객체가 가질 수 있는 상태가 뭐가 있는지 살펴보자.

상태의미상태 변경 조건
pending비동기 처리가 아직 수행되지 않은 상태프로미스가 생성된 직후 기본 상태
fulfilled비동기 처리가 수행된 상태(성공)resolve 함수 호출가 되면
rejected비동기 처리가 수행된 상태(실패)reject 함수 호출가 되면

즉, 프로미스 객체는 인수로 받은 콜백함수의 내부에서 콜백함수가 첫 번째 인수로 받은 콜백함수가 실행되면 성공으로 간주하고 fulfilled상태가 되고, 두 번째 인수로 받은 콜백함수가 실행되면 실패로 간주하고 rejected상태가 된다.

프로미스 객체는 상태값 외에도 처리 결과를 갖는데 만약 fulfilled 상태의 프로미스 객체라면 value값으로 성공한 결과 값을 갖고, rejected 상태의 프로미스 객체라면 value값으로 실패한 이유가 담긴 Error 객체를 갖게 된다. 이처럼 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체이다.

프로미스 객체의 후속 처리

앞에서 프로미스 객체는 비동기 처리에 대한 처리 상태를 가진다고 했다. 이때 상태에 따라 각기 다른 처리를 할 수 있는 후속 처리 메서드가 있는데 다음과 같다.

  1. then
    then 메서드는 프로미스 객체 뒤에 .으로 연결해서 사용하며 두 가지 콜백 함수를 인수를 전달받는다.
    첫 번째 콜백 함수는 앞선 프로미스 객체가 fulfilled 상태가 되면 호출되며, 두 번째 콜백 함수는 앞선 프로미스 객체가 rejected 상태가 되면 호출된다. 이때 각 콜백 함수는 앞선 프로미스 객체의 결과값을 인수로 받는다. 또한 then 메서드는 언제나 프로미스 객체를 반환하여 뒤에 이어 다른 후속 처리가 가능하게 한다.
  2. catch
    catch 메서드는 then 메서드와 마찬가지로 프로미스 객체 뒤에 .으로 연결해서 사용하며 하나의 콜백 함수를 인수로 받는다. catch 메서드는 앞선 프로미스 객체가 rejected 상태가 될 때만 호출되며 인수로 받은 콜백 함수를 실행시킨다. 이때 콜백 함수는 앞선 프로미스 객체의 결과값 그러니까 catch의 경우 Error객체를 인수로 받는다. 마찬가지로 언제나 프로미스 객체를 반환하여 뒤에 이어 다른 후속 처리가 가능하게 된다.
    사실상 catch는 then메서드에서 첫 번째 콜백 함수 자리를 공백으로 두고 두 번째 콜백 함수만 받았을 때와 동일하게 동작한다.
  3. finally
    finally 메서드는 앞선 프로미스 객체의 상태에 상관없이 실행되는 콜백함수를 콜백함수로 받는다.
// url을 받아 해당 url에 GET요청을 보내고 성공하면 성공 결과를 담은 fulfilled 상태의 프로미스 객체를,
// 실패하면 에러 내용을 담은 Error 객체를 담은 rejected 상태의 프로미스 객체를 반환하는 promiseGet 함수 작성
const promiseGet = url => {
  return new Promise((resole, reject) => {
    const xhr = new XMLHeepRequest();
    xhr.open("GET", url);
    xhr.send();
    
    xhr.onload = () => {
      if(xhr.status === 200) {
        resolve(JSON.parse(xhr.response));
      } else {
        reject(new Error(xhr.status));
      }
    };
  });
};

// 구글 url를 상대로 promiseGet 메서드를 실행하고 fulfilled 상태의 프로미스 객체가 반환되면 then메서드의 콜백함수가,
// rejected 상태의 프로미스 객체가 반환되면 catch메서드의 콜백함수가 실행되고 상태에 상관없이 마지막에 finally메서드의 콜백함수가 실행된다.
promiseGet('www.google.com')
  .then(res => console.log(res))
  .catch(err => console.log(err))
  .finally(() => console.log('Done'));

위에서 봤듯이 프로미스 객체를 사용하면 비동기 처리에서의 에러를 처리할 수 있는데 처리할 수 있는 방법이 두 가지가 있었다. 첫 번째는 then 메서드의 두 번째 콜백 함수를 이용하는 것과 두 번재는 catch 메서드를 사용하는 것이었는데 둘은 동일하게 동작하지만 약간의 차이가 있다.
바로 then 메서드의 두 번째 콜백 함수를 이용한 오류 처리는 바로 앞선 프로미스 객체를 반환하는 비동기 처리의 에러만 처리가 가능하지만 catch 메서드를 통한 에러처리는 프로미스 체이닝 전체에 걸처 모든 에러를 처리할 수 있다는 것이다.
따라서 각 작업 결과와 단계에 따라서 오류에 대해 처리하는 것이 다르다면 then 메서드를 통해 각 단계마다 에러를 처리하는 것이 바람직하고, 전체에 걸체 에러가 발생하기만 하면 동일하게 처리할 내용은 catch 메서드를 사용하는 것이 바람직하다.

프로미스의 정적 메서드

프로미스 객체가 제공하는 몇 가지 정적 메서드가 있다.

  1. Promise.resolve(인수), Promise.reject(인수)
    이미 상태와 결과 값을 가지고 있는 프로미스 객체를 만들어낸다. 즉 Promise.resolve 메서드는 인수로 들어온 값을 결과 값으로 가지는 fulfilled 상태의 프로미스 객체를 만들고, Promise.reject 메서드는 인수로 들어온 에러 객체를 결과 값으로 가지는 rejected 상태의 프로미스 객체를 만든다.

  2. Promise.all(인수)
    여러 개의 비동기 처리를 모두 병렬로 처리할 때 사용하며 인수로는 프로미스 객체를 요소로 갖는 배열를 받는다. 그리고 인수로 받은 배열에 담긴 모든 프로미스 객체의 상태가 fulfilled 상태가 되면 그때 각 프로미스 객체들의 결과값을 배열에 담은 것을 결과 값으로 갖는 프로미스 객체를 반환한다. 만약 하나라도 rejected 상태가 되면 Promise.all 메서드는 그 즉시 종료된다.

  3. Promise.race(인수)
    all 메서드와 비슷하게 여러 개의 비동기 처리를 모두 병렬로 처리할 때 사용하며 인수로는 프로미스 객체를 요소로 갖는 배열를 받는다. 하지만 인수로 받은 배열에 담긴 모든 프로미스 객체들의 처리를 기다리는 것이 아니라 가장 먼저 fulfilled 상태가 된 프로미스 객체의 결과값을 결과값으로 갖는 프로미스 객체를 반환한다. 마찬가지로 하나라도 rejected 상태가 되면 Promise.race 메서드는 그 즉시 종료된다.

  4. Promise.allSettled(인수)
    all 메서드와 비슷하게 여러 개의 비동기 처리를 모두 병렬로 처리할 때 사용하며 인수로는 프로미스 객체를 요소로 갖는 배열를 받는다. 하지만 인수로 받은 배열에 담긴 모든 프로미스 객체의 상태가 결정되기만 하면, 즉 fulfilled 상태나 rejected 상태가 되면 그때 각 프로미스 객체들의 결과값을 배열에 담은 것을 결과 값으로 갖는 프로미스 객체를 반환한다.

마이크로태스크 큐

앞서 비동기 처리에 대한 콜백 함수들은 태스크 큐라는 곳에 보관되어 이벤트 루프에 의해 실행 컨텍스트 스택이 비었을 때 하나씩 이동되어 실행된다고 했다. 하지만 프로미스 객체에서 사용하는 콜백 함수들은 태스크 큐가 아닌 마이크로태스크 큐라는 곳에 보관된다. 마이크로태스크 큐태스크 큐와 실행되는 원리는 동일하지만 우선순위가 더 높다. 즉, 이벤트 루프는 콜 스택(실행 컨텍스트 스택)이 비어지면 태스크 큐보다 마이크로태스크 큐를 먼저 살펴보고 실행해야 할 메서드들이 있다면 실행시킨다. 이후 마이크로태스크 큐가 비면 태스크 큐에서 대기하고 있는 메서드를 가져와 실행한다.

fetch

이전 포스트의 끝자락에서 잠깐 살펴봤던 fetch 메서드는 HTTP 요청 전송 기능을 제공하는 클라이언트 사이트 Web API이다. fetch 메서드는 HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환하는데 이를 이용하면 서버와의 통신을 이용한 다양항 API를 손쉽게 사용할 수 있다. 이때 주의해야 할 점은 응답으로 받은 Promise객체는 만약 성공했다면 결과값을 가지고 있을 것이다. 하지만 이 결과값은 JSON 형태의 결과이니 꼭 역직렬화를 거친 다음, 자바스크립트에서 사용해야 한다.

profile
I Will be Relaxed Person

0개의 댓글