프로미스 in JS

동동·2021년 8월 17일
2

1. 이벤트 루프

JS 엔진은 싱글 스레드 이벤트 루프 개념을 기반으로 한다.
한 번에 한개의 코드만 실행할 수 있으며, 실행 예정인 코드는 Task Queue에 유지된다. 코드는 실행될 준비가 될 때마다 Task Queue에 추가된다.
JS 엔진이 코드 실행을 마치면, 이벤트 루프는 Task Queue에서 다음 작업을 꺼내 실행한다.

2. 이벤트 모델

어떤 이벤트가 발생하면 실행될 함수를 미리 등록해둔다.

이벤트가 발생하면 함수가 Task Queue의 맨 뒤에 추가된다.

앞선 Task Queue의 모든 작업들이 완료된 후에 실행된다.

간단한 인터랙션에서는 적합하지만, 여러 개의 비동기 호출이 연결되어 있을 때에는 제어가 복잡해진다는 단점이 있다.

3. 콜백 패턴

콜백 함수를 비동기 함수의 인자로 전달하여, 비동기 함수의 실행이 완료된 후 Task Queue의 맨 뒤에 콜백 함수와 콜백 함수의 인자를 가진 새로운 작업을 추가한다. 그 작업은 모든 다른 작업 완료 후 실행된다.

콜백 패턴은 여러 개의 호출 연결이 쉽기 때문에 이벤트보다 더 유연하다.
다만, 너무 많은 콜백의 중첩이 발생할 수 있다. (콜백 헬)

4. 프로미스

  • 프로미스는 비동기 연산의 결과를 위한 플레이스홀더입니다.

  • 프로미스는 3가지의 상태를 가질 수 있습니다.

    • pending: 프로미스가 생성되고 난 후의 초기 상태입니다. fulfilled와 rejected가 아닌 상태입니다.
    • fulfilled: 비동기 연산이 성공적으로 실행된 상태입니다.
    • rejected: 비동기 연산이 실패한 상태입니다.

    ※ pending인 상태를 프로미스가 미확정이다(unsettled) 라고 하며, fulfilled 또는 rejected 상태인 프로미스는 확정된(settled) 프로미스라고 합니다.

  • 프로미스의 상태는 프로미스의 내부 프로퍼티 [[PromiseState]]에 설정되며, 이 내부 프로퍼티에 직접적으로 접근할 수 있는 방법은 없습니다.

  • 다만, then 메서드와 catch 메서드를 사용하여 프로미스의 상태가 변경될 때 특정 동작을 취하도록 할 수 있습니다. 마이크로 태스크 큐에 추가된다.

then 메서드

  • then 메서드는 두 개의 인자를 받습니다. 첫번째 인자는 프로미스가 성공했을때 호출할 함수(성공 핸들러)이며, 두번째 인자는 프로미스가 실패했을 때 호출할 함수(실패 핸들러)입니다.

  • then()이나 catch() 호출은 또 다른 프로미스를 만들어 반환합니다. 따라서 then과 catch 를 호출으로 반환된 프로미스의 then, catch를 호출하여 Promise chain을 이어갈 수 있습니다.

catch 메서드

  • catch 메서드는 실패 핸들러만 받는 then 메서드와 기능적으로 동일합니다.
  • 이벤트는 이벤트 실행시 에러가 발생하면 이벤트 핸들러가 호출되지 않는다.
    콜백 함수는 항상 에러 인자 검사를 수행하여야 한다.
    프로미스는 실패 핸들러를 추가하면 실패 시에 항상 호출된다.

작업 스케쥴링

resolve()나 reject()가 Promise 실행자 내에서 호출되면 프로미스를 처리하기 위해 마이크로 태스크 큐에 작업을 추가한다. 프로미스 실행자는 동기적으로 바로 실행된다.

실행자 에러

실행자 내에서 반드시 reject를 호출하지 않아도, 실행자에서 에러가 발생하면 프로미스의 실패 핸들러가 호출된다.
실행자에서 발생한 에러는 실패 핸들러가 존재할 때만 전달된다. 그렇지 않으면 에러는 숨겨진다.

전역 프로미스 실패 처리

프로미스가 실패 핸들러 없이 실패했을 때, unhandledRejection 이벤트와 rejectionHandled 이벤트가 발생한다.

  • unhandledRejection: 프로미스가 실패하고 같은 이벤트 루프 턴에서 실패 핸들러가 호출되지 않으면 발생
  • rejectionHandled: 프로미스가 실패하고 이벤트 루프의 턴 이후 실패 핸들러가 호출되면 발생

프로미스는 값(플레이스홀더)이므로, 실패한 시점에 반드시 catch() 를 호출하지 않고 어느 정도 시간이 흐른 후에 catch()를 호출할 수 있다. 즉, 프로미스는 즉시 실패하지만 특정 시점까지 처리되지 않을 수 있기 때문에, 프로미스 실패가 처리되었는지 판단하는 것은 간단하지 않다.

Promise.all

프로미스의 이터러블을 인자로 받아 프로미스 하나를 반환한다.
이터러블 내 모든 프로미스가 성공하면 반환된 프로미스는 성공한다.
성공 핸들러의 인자에는 각 프로미스의 처리된 값의 배열이 전달되며, 그 값들은 프로미스들이 처리된 순서대로 저장되므로 프로미스 결과와 처리된 프로미스를 일치시킬 수 있다.

이터러블 내 하나의 프로미스라도 실패하면 반환된 프로미스는 즉시 실패한다. 실패 핸들러는 항상 배열이 아닌 하나의 값을 받고 그 값은 실패한 프로미스의 실패 값이다.

Promise.race

프로미스의 이터러블을 받고 프로미스 하나를 반환한다.
반환된 프로미스는 첫 번째 프로미스가 확정되자마자 확정된다.
Promise.all() 메서드처럼 모든 프로미스가 성공하기를 기다리지 않고, 이터러블 내 어떤 프로미스라도 성공하면 바로 그에 맞는 프로미스를 반환한다.

최초에 확정된 프로미스가 성공하면 반환된 프로미스는 성공하고, 최초에 확정된 프로미스가 실패하면 반환된 프로미스 또한 실패한다.

Q. Promise.race 인자 내 두번째로 확정된 프로미스가 실패하고, 반환된 프로미스의 실패 핸들러가 없다면 UnhandledRejection 이벤트가 발생하는가?

Follow-up

Task Queue vs Micro Task Queue

profile
작은 실패, 빠른 피드백, 다시 시도

4개의 댓글

comment-user-thumbnail
2021년 8월 18일

프로미스 설명 보려고 들어왔는데, 이벤트 루프 설명에 감동하고 갑니다. 좋은 표현 감사합니다 👍

1개의 답글
comment-user-thumbnail
2021년 8월 18일

이 글도 나의 단어로 풀어서 설명하는 시리즈에 포함되는거죵? 이 시리즈 너무 좋아요~~~ 근데 시리즈에서는 이 글이 조회되지 않는 걸 보면 카테고라이징이 안되어있는게 아닐까 싶어요 :)

1개의 답글