[TIL] Promise

May 29, 2021

프로젝트를 진행하며 OAuth를 통한 회원가입과 로그인을 한 번에 처리해야할 필요가 발생했다. SNS 계정으로 처음 서비스를 이용하는 경우, 자동으로 회원가입을 시킨 뒤에 로그인까지 바로 이어서 시켜줘야 하기 때문이다. 그러다 보니,

  1. sns 계정으로 로그인을 클릭한 사용자의 정보가 DB에 있는지 확인하고
  2. 없을 경우(= 최초 방문자일 경우) 해당 사용자의 정보를 회원 DB에 추가한 뒤
  3. 다시 1에서 로그인을 클릭한 사용자의 정보를 회원 DB에서 조회하여 로그인 처리

의 과정을 거치며 DB에 query를 날릴 때, 콜백의 콜백의 콜백 함수를 사용하게 됐다. 코드의 가독성을 높이기 위해 이번 기회에 자바스크립트에서의 비동기 처리 방식들에 대해 공부해보고자 한다.

우선 자바스크립트에서 비동기적으로 실행되는 코드는 비동기가 아닌 모든 코드(전역 코드 및 명시적으로 호출된 함수 등)가 전부 실행되고 난 이후에 비로소 실행된다. 그 이유를 알기 위해서는 자바스크립트의 실행 컨텍스트, 콜 스택, 태스크 큐 등에 대한 이해가 필요하다. 자바스크립트의 동작 순서를 간략하게 설명하면 다음과 같다.

  1. 코드가 평가되어 생성된 실행 컨텍스트가 콜 스택에 추가된다.
  2. 이벤트 핸들러, HTTP 요청, setTimeout 등의 비동기 함수들도 이 과정에 콜 스택에 추가되고, 실행되면서 비동기 처리를 위한 콜백 함수를 ‘호출 스케줄링’한 뒤에 콜 스택에서 제거된다.
  3. 이때, 스케줄링 된 콜백 함수는 브라우저에 의해 태스크 큐에 추가된다. *태스크 큐는 콜 스택이 완전히 비어있어야만 호출된다.
  4. 이벤트 루프는 콜 스택이 비어있는지 체크한다. 모든 코드가 실행 완료되어 콜 스택이 비게 되면, 이벤트 루프는 태스크 큐에서 대기 중이던 비동기 함수의 콜백 함수를 콜 비로소 콜 스택에 푸시한다.
  5. 콜 스택에 푸시된 콜백 함수는 실행된 뒤 콜 스택에서 제거된다.

이런 과정을 거치는 탓에, 비동기 함수의 처리 결과를 외부로 반환하거나, 상위 스코프에 있는 변수에 할당할 수 없게 된다. 비동기 함수의 콜백 함수가 호출되는 시점(5)에는 이미 다른 코드들의 실행이 종료되어 콜 스택에서 제거된 상태이기 때문이다.

따라서 비동기 함수의 호출 결과에 대한 후속 처리는 비동기 함수의 내부에서 또다른 콜백함수를 통해 처리되어야 한다. 처리가 성공했을 때 또는 실패 했을 때, 추가적 처리가 더 필요할 때 등등 후속 처리가 많아지는 경우에는 콜백 함수가 계속 중첩되어 코드의 가독성이 매우 낮아진다. 이러한 현상을 ‘콜백 헬’이라고 한다.

콜백 헬 외에도 비동기 처리는 try..catch 문 처럼 에러 처리를 할 때도 어려움을 발생시킨다. 에러는 호출자 방향(=콜 스택의 아래 방향)으로 전파되는데, 비동기 처리가 실행되는 시점에 이미 try 블록의 비동기 함수는 실행이 완료되어 콜 스택에서 제거된 상태이다. 따라서 catch 블록에서 에러를 캐치하지 못한다.

Promise

ES6에서는 비동기 처리의 문제를 극복하기 위한 방법으로 Promise가 도입되었다.

프로미스는 비동기 처리의 상태결과를 관리하는 객체이다. 프로미스 생성자 new Promise에 의해 호출되는 함수인 executor는 resolvereject 함수를 인수로 전달받는다. 비동기 처리가 성공했을 때에는 resolve 함수가 호출되고, 실패했을 때에는 reject 함수가 호출된다.

let promise = new Promise((resolve, reject) => {
    // 비동기 처리 코드
    if(success) {
        resolve("비동기 처리 성공!");
    } else {
        reject("비동기 처리 실패!");
    }
})

// 비동기 처리 성공!

생성된 프로미스는 내부적으로 ‘상태 [[PromiseStatus]]‘와 ‘결과[[PromiseValue]]’ 값을 갖는다. 이때, [[PromiseStatus]]는 세 가지 상태 값 중 하나를 갖고, [[PromiseValue]]에는 resolve 또는 reject 콜백 함수의 값이 담긴다.

pending : 비동기 처리 수행 전의 상태
fulfilled : 비동기 처리가 성공한 상태
rejected : 비동기 처리가 실패한 상태

프로미스의 콜백 함수인 resolve나 reject가 실행되면 그 결과를 가지고 후속 처리가 필요하다. 이때, 프로미스의 후속 처리를 도와주는 메서드로는 .then .catch .finally 가 있다.

.then
두 개의 콜백 함수를 인수로 전달받는다. 첫 번째 콜백 함수는 비동기 처리가 성공하여 resolve 함수가 실행되어 프로미스의 상태가 fulfilled로 바뀌면 호출된다. 이때, resolve의 결과가 인수로 전달된다. 두 번째 콜백 함수는 reject 함수가 실행되어 rejected 상태일 때 호출된다. reject의 결과 즉, 에러가 인수로 전달된다.

.catch
한 개의 콜백 함수를 인수로 전달받는다. 프로미스가 rejected 상태인 경우에만 호출된다. catch 메서드는 내부적으로 .then(undefined, onRejected)와 동일하게 작동한다!

.finally
프로미스의 성공 또는 실패와 상관 없이 무조건 한 번 호출된다. 따라서 프로미스의 상태와 상관 없이 공통적으로 처리할 내용이 있을 때 사용된다. finally 뒤에 then이 사용되는 경우 result와 error는 finally를 그대로 통과하여 then에 전달된다!

모든 후속 처리 메서드는 다음과 같은 특징을 갖는다.

  • 후속 처리 메서드는 다시 프로미스를 반환한다. 따라서 후속 처리 메서드를 연속적으로 호출하는 “프로미스 체이닝”이 가능하다.
  • 후속 처리 메서드의 콜백 함수가 프로미스가 아닌 값을 반환하더라도 그 값을 암묵적으로 resolve 또는 reject 하여 프로미스를 생성해서 반환한다.
  • 후속 처리 메서드는 비동기로 동작 한다.

이처럼 프로미스는 비동기 처리에 있어서 발생하는 콜백 지옥을 완화하는 데 도움을 준다. 그러나 프로미스의 후속 처리 메서드들도 결국 콜백 함수를 사용하므로 결국 비슷한 문제를 발생시킬 수 있다.

이러한 프로미스의 한계를 보완하기 위해 ES8에서 등장한 것이 async/await 이다.

참고

모던 JavaScript 튜토리얼
MDN Web Docs
모던 자바스크립트 Deep Dive | 위키북스 | 이웅모


Profile picture

42KIM A person trying to create something.
👉Github