ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Group Study, 모던 자바스크립트 Deep Dive] - 45 Promise
    Front-end/Javascript 2023. 4. 9. 16:26
    반응형

    Group study background

    나만 그런건지는 모르겠지만, 실무를 하다보면 잊어버리는 개념들이 있다.

    가끔 FE 뉴비인분들에게 질문을 받는데, 아리송 할때만큼 쪽팔릴때가 없었다.

    인간은 망각의 동물이라고 교수님께서 말씀하셨지만 반복 학습의 힘을 믿는다. React 오픈카톡방에서 모집한 스터디원분들과 함께 "모던 자바스크립트 Deep Dive" 1권 톺아보기를 시작한다!

     

    정보 전달용이 아닌 개인 스터디 레코딩용 포스트입니다.


    45.0 Preface

    JS는 비동기 처리를 위해 콜백 함수를 사용한다. 하지만 Callback Hell로 가독성이 지옥으로 가버리거나 에러 핸들링의 어려움이 있다. 이 단점을 극복하기 위해 ES6에서는 비동기 처리를 위해 Promise 🤝(약속)을 도입했다. 비동기 처리 시점을 명확하게 표현하게 되면서 코드의 가독성을 끌어올릴 수 있게 되었다.


     

    45.1 비동기 처리를 위한 콜백 패턴의 단점

    45.1.1 Callback Hell

     

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

     

    [예제 45-01]

    const get = url => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.send();
      
      xhr.onload = () => {
        if (xhr.status === 200) {
         return JSON.parse(xhr.response)
        } else {
         console.log(`${xhr.status} ${xhr.statusText}`)
        }
      }
    }
    
    
    const response = get('https://jsonplaceholder.typicode.com/posts/1');
    console.log(response)

    위의 예시는 JS를 배우는 단계에서 꽤나 헷갈려하기도 하고 찾아보는 부분이 아닐까 싶다.  console.log에 반환 값을 찍으면 undefined가 찍힌다. get 함수 내부의 onload 이벤트 핸들러가 비동기로 동작하기 때문이다.

    get 함수는 반환문이 생략되었으므로 암묵적으로 undefined를 반환하게 된다.

    1. 비동기 함수 get이 호출되면 함수 코드를 평가하는 과정에서 get 함수의 실행 컨텍스트 생성, 실행 컨택스트 스택에 푸시
    2. 함수 코드 실행 과정에서 xhr.onload 이벤트 핸들러 프로퍼티에 이벤트 핸들러 바인딩
    3. get 함수 종료시 get 함수의 실행 컨텍스트가 콜 스택에서 팝
    4. 곧바로 console.log(response) 출력
    5. console.log 실행 컨텍스트가 생성되어 실행 컨텍스트 스택에 푸시
      1. console.log가 호출되기 직전에 load 이벤트가 발생했더라도 xhr.onload 이벤트 핸들러 프로퍼티에 바인딩한 이벤트 핸들러는 먼저 실행되지 않음
    6. 서버로부터 응답이 도착해야 xhr 객체에서 load 이벤트 발생
      1. xhr.onload 핸들러 프로퍼티에 바인딩한 이벤트 핸들러는 load 이벤트가 발생하면 태스크 큐에 저장되어 대기하다가, 콜 스택이 비면 이벤트 루프에 의해 콜 스택으로 푸시되어 실행.

    따라서 비동기 함수는 처리 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수 없다. 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 수행해야 하는데 콜백 함수를 전달하는 것이 일반적이다.

     

    [예제 45-02] 진정한 callback hell

    const get = (url, callback) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.send();
      
      xhr.onload = () => {
        if (xhr.status === 200) {
         callback(JSON.parse(xhr.response))
        } else {
         console.log(`${xhr.status} ${xhr.statusText}`)
        }
      }
    }
    
    
    const url = get('https://jsonplaceholder.typicode.com/');
    
    
    // 그나마 귀여운...
    get(`${url}/posts/1`, ({userId}) => {
      console.log(userId);
      
      get(`${url}/users/${userId}`, userInfo => {
       console.log(userInfo)
      })
    })
    
    // going to the hell
    get('/step1', a => {
     get(`/step2/${a}`, b => {
      get(`/step3/${b}`, c => {
       get(`/step4/${c}`, d => {
        console.log(d)
       })
      })
     })
    })

     

    45.1.2 에러 처리의 한계

    [예제 45-03]

    try {
     setTimeout(() => { throw new Error('Error!'); }, 1000)
    } catch (e) {
     // 캐치 불가
     console.error('캐치한 에러', e)
    }

    비동기 함수인 setTimeout이 호출되면

    1. setTimeout 함수의 실행 컨택스트 생성되어 콜스택에 푸시
    2. setTimeout은 비동기 함수이므로 콜백 함수 호출을 기다리지 않고 즉시 종료되어 콜 스택에서 제거
    3. 타이머가 만료되면 setTimeout 함수의 콜백 함수는 태스크 큐로 푸시되고 콜 스택이 비어졌을 때 이벤트 루프에 의해 콜 스택으로 푸시되어 실행

    즉, setTimeout 함수의 콜백 함수가 실행될 때 setTimeout 함수는 이미 콜 스택에서 제거된 상태이다. setTimeout 함수의 콜백 함수를 호출한 caller가 setTimeout 함수가 아님을 의미한다.

    에러는 호출자(caller) 방향으로 전파된다. 다시 말하면, 콜 스택의 아래 방향(실행 중인 실행 컨텍스트가 푸시되기 직전에 푸시된 실행 컨택스트 방향)으로 전파된다. 하지만 위의 예시와 같이 setTimeout 함수의 콜백 함수를 호출한 것은 setTimeout이 아니기 때문에, catch 블록에서 잡을 수 없다.


    45.2 Promise 생성

    Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 인수로 resolve, reject 함수를 전달 받는다. 그리고 비동기 처리 상태와 처리 결과를 관리하는 객체이다.

     

    [예제 45-04]

    const promise = new Promise((reoslve, reject) => {
     if(성공) {
      resolve('result')
     } else (
      reject('failure')
     )
    })
    프로미스의 상태 정보 의미 상태 변경 조건
    pending 비동기 처리가 아직 수행되지 않은 상태 프로미스가 생성된 직후 기본 상태
    fulfilled 비동기 처리가 수행된 상태(성공) resolve 함수 호출
    rejected 비동기 처리가 수행된 상태(실패) reject 함수 호출


    45.3 Promise 후속 처리 메서드

    프로미스의 비동기 처리 상태가 변화하면 이에 따른 후속 처리를 해야 한다. 예를 들어 fullfilled 상태가 되면 처리 결과로 어떤 로직을 사용할테고, reject가 되어도 에러 처리를 해야 한다. 이를 then, catch, finally를 통해 실행한다.


    45.4 Promise 에러 처리

    [예제 45-05]

    const wornUrl = 'https://jsonplaceholder.typicode.com/XXX/1'
    
    promiseGet(wrongUrl)
     .then(res => console.log(res))
     .catch(err => console.error(err))
     
     // 내부적으로는 아래와 같이 처리 됨.
     promiseGet(wrongUrl)
     .then(res => console.log(res))
     .then((undefined, err => console.error(err))

    45.5 Promise Chainning

    비동기 처리를 위한 콜백 패턴에 Callback HEEEEELLL이 발생하는 문제가 있다. 

    예제 45-02를 아래 예제처럼 프로미스를 연속적으로 호출하는 Promise Chainning을 통해 해결 할 수 있다.

    [예제 45-06]

    const url = 'https://jsonplaceholder.typicode.com'
    
    promiseGet(`{url}/posts/1`)
     .then(({userId}) => promiseGet(`{url}/users/${userId}`))
     .then(userInfo => console.log(userInfo))
     .catch(err => console.error(err))

    만약 후속 처리 메서드의 콜백 함수가 프로미스가 아닌 값을 반환하더라도 그 값을 암묵적으로 resolve, reject하여 프로미스를 생성해 반환한다.  Promise Chainning을 통해 비동기 처리 결과를 전달받아 후속 처리를 하므로 비동기 처리를 위한 콜백 패턴에서 발생하던 callback hell이 발생하지 않는다.

     


    45.6 Promise의 정적 메서드

    45.6.2 Promise.all

    Promise.all은 여러 개의 비동기 처리를 모두 병렬 처리할 때 사용하는데 인수로 전달받은 배열의 모든 프로미스가 모두 fulfilled 상태가 되어야 종료한다. 따라서 Promise.all 메서드가 종료하는 데 걸리는 시간은 가장 늦게 fulfilled 상태가 되는 프로미스의 처리 시간보다 조오금 더 길다.

    그리고 모든 프로미스가 fulfilled 상태가 되면 resolve된 처리 결과를 모두 배열에 저장해 새로운 프로미스를 반환한다. 이때 첫 번째 프로미스가 가장 나중에 fulfilled 상태가 되어도 첫 번째 프로미스 처리 결과부터 차례대로 배열에 저장해 그 배열을 resolve하는 새로운 프로미스를 반환한다. 즉, 처리 순서가 보장된다.

    인수로 전달받은 배열의 프로미스가 하나라도 reject 상태가 되면 나머지 프로미스가 fulfilled 상태가 되는 것을 기다리지 않고 즉시 종료한다.

    45.6.3 Promise.race

    Promise.all 메서드처럼 모든 프로미스가 fulfilled 상태가 되는 것을 기다리는 것이 아니라 가장 먼저 fulfilled 상태가 된 처리 결과를 resolve 한다.

    45.6.4 Promise.allSettled

    Promise.allSettled 메서드는 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달받는다. 그리고 전달받은 프로미스가 모두 setteled 상태가 되면 처리 결과를 배열로 반환한다.

    [예제 45-07]

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

    + Promise.any

    ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any


    45.7 마이크로태스크 큐

    프로미스의 후속 처리 메서드도 비동기로 동작하므로 1->2->3 순으로 출력될 것처럼 보이지만 2->3->1 순으로 출력된다. 

    그 이유는 프로미스의 후속 처리 메서드의 콜백 함수는 태크스 큐가 아니라 마이크로태스크 큐에 저장되기 때문이다. 마이크로태스크 큐는 태스크 큐와 별도로 프로미스의 후속 처리 메서드의 콜백 함수가 일시 저장되는 곳이다. 

    콜백 함수나 이벤트 핸들러를 일시 저장한다는 점에서 태스트 큐와 동일하지만 마이크로 태스크 큐는 태스크큐보다 우선순위가 높다. 즉 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에서 대기하고 있는 함수를 가져와 실행한다. 

    [예제 45-08]

    setTimeout(() => console.log(1), 0);
    
    Promise.resolve()
     .then(() => console.log(2))
     .then(() => console.log(3))

    45.8 fetch

    fetch 함수는 XMLHttpRequest 객체와 마찬가지로 HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 WEB API다. XMLHttpRequest 객체보다 사용법이 간단하고 프로미스를 지원하므로 콜백 패턴에서 자유로워 많이 사용된다.

     

     

     

    추천하는 Reference: https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke

     

     

    반응형

    댓글

Designed by Tistory.