[TIL] 8/23

예도리·2021년 8월 23일
0
post-thumbnail

비동기 처리

동기식 처리는 일을 직렬적으로 처리한다. 한 가지 일을 끝내고 다른 일을 순차적으로 진행한다. 이것의 문제점은 지금 하고 있는 작업이 끝나기 전까지는 이후 작업들이 blocking된다는 것이다.

예를 들어, 서버와 통신해서 데이터를 받아오고 화면에 렌더링하는 과정을 동기식 처리로 진행할 경우, 서버에 데이터를 요청해서 받아오는 동안은 이후 작업들이 모두 blocking되기 때문에 렌더링되지 않는다. 그렇게 되면 사용자는 빈 화면을 만나게 될 것이다.

이와 다르게 비동기 처리는 작업이 종료되지 않았더라도 다른 작업을 실행한다. 서버에 데이터를 요청하고 가만히 기다리는 것이 아니라 다른 작업을 진행하다가 데이터가 오면 다시 작업을 재개한다.

위의 예시를 비동기 처리로 진행했다면 데이터를 기다리는 동안 먼저 화면에 렌더링해도 되는 요소들이 렌더링돼서 사용자가 빈 화면을 만나지 않게끔 할 수 있다.

Promise

Promise는 ES6에서 도입된 개념으로, 비동기 처리에 사용되는 객체다.
기존의 콜백 패턴이 가진 단점: 콜백헬, 에러 처리의 한계를 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.

콜백 헬

비동기 처리 모델은 작업을 완료하지 않아도 다음 태스크를 실행하기 때문에 코드에 작성된 순서대로 동작한다는 것을 보장할 수 없다. 따라서 처리 순서를 맞춰야하는 경우, 예를 들어 어떤 비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해야하는 경우, 콜백 함수를 계속 중첩(nesting)시켜야한다. 이 때 콜백 헬이 발생하게 되는데, 가독성도 매우 안좋고 복잡도가 증가하고 에러 처리가 곤란하다.

Promise 생성

Promise 객체는 생성자 함수로 생성한다. 다른 객체와 마찬가지로 생성자 함수에 new를 붙여서 생성하면 된다. Promise 객체는 4가지의 state 정보를 갖는다.

  • pending: 비동기 처리가 아직 수행되지 않음
  • fulfilled: 비동기 처리가 성공한 상태
  • rejected: 비동기 처리가 실패한 상태
  • settled: 비동기 처리가 성공/실패한 상태

Promise 생성자 함수는 비동기 처리 결과에 따라 실행하는 콜백 함수를 인자로 전달받는다. 내부에서 비동기 처리 작업을 수행한 다음 비동기 처리가 성공하면 인자로 전달 받은 resolve 함수를 호출한 뒤 fulfilled 상태가 되고, 실패하면 reject 함수를 호출한 뒤 rejected 상태가 된다.

// Promise 객체 생성
  new Promise((resolve, reject) => {
    /*
    * ... 비동기 처리 구현 ...
    */
    if (/* 성공 */) {
      // resolve 메소드를 호출하면서 처리 결과를 전달
      resolve(/* 처리 결과 */); // 이 처리 결과는 Promise 객체의 후속 처리 메소드로 전달됨
    } else {
      // reject 메소드를 호출하면서 에러 메시지를 전달
      reject(new Error(/* status */)); // 이 에러 메세지는 후속 처리 메소드로 전달됨
    }
  });

후속 처리 메소드

Promise의 비동기 함수는 Promise 객체를 반환한다. 이 비동기 함수를 호출한 caller에서 Promise 객체의 후속 처리 메소드를 통해 처리 결과나 에러 메시지를 전달받아 처리한다.

  • then: 두 개의 콜백 함수를 인자로 전달 받는다. 하나는 Promise 객체가 성공 상태일 때, 다른 하나는 실패 상태일 때 호출된다. then은 Promise를 반환한다.
  • catch: 비동기 처리와 then 메소드에서 에러가 발생하면 호출된다. Promise를 반환한다.

비동기 처리에서 에러가 발생했을 때 then 메서드의 두 번째 콜백 함수나 catch를 사용해서 처리한다. catch 메서드는 내부적으로 첫 번째 인자를 undefined를 넘겨주는 then을 호출한다.

//then 단독 사용
promiseFunction(/* 주소 */)
  .then(res => console.log(res), err => console.error(err));
// 이 경우 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못함 ㅠㅠ

// then과 then 사용
promiseFunction(/* 주소 */)
  .then(res => console.log(res))
  .then(undefined, err => console.error(err)); 

// then과 catch 사용
promiseFunction(/* 주소 */)
  .then(res => console.log(res))
  .catch(err => console.error(err)); 

이렇게 3가지 방법으로 사용할 수 있으나

  1. then 메서드에 두 개의 콜백 함수를 넘겨 사용하는 경우 두 번째 콜백 함수가 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못한다
  2. 코드가 복잡해져서 가독성이 좋지 않다
  3. then 다음에 catch를 호출하면 비동기 처리 중 발생한 에러 뿐만 아니라 then에서 발생한 에러까지 캐치할 수 있다!
  4. catch 메서드를 사용해야 가독성이 좋고 의미 전달이 명확해진다

의 이유로 then 메서드로 전부 처리하는 게 아닌 then을 호출하고 catch를 호출하는 방식을 권장한다고 한다.

정적 메소드

Promise 객체는 4개의 정적 메소드를 가지고 있다.

Promise.resolve, Promise.reject

Promise.resolve와 Promise.reject는 인자로 전달된 값을 Promise로 감싼다. 단, Promise.resolve 메소드는 값을 resolve하는 Promise를 생성하고, Promise.reject는 값을 reject하는 Promise를 생성한다.

// resolve1과 resolve2는 동일하게 동작함
const resolve1 = Promise.resolve(/* 값 */);
const resolve2 = new Promise(resolve => resolve(/* 값 */));
resolve1.then(console.log); // 값

// reject1과 reject2는 동일하게 동작함
const reject1 = Promise.reject(new Error('에러'));
const reject2 = new Promise((resolve, reject) => reject(new Error('에러')));
reject1.catch(console.log) // Error: 에러

Promise.all, Promise.race

Promise.all과 Promise.race는 Promise가 담겨 있는 이터러블을 인자로 받는다.

Promise.all은 전달받은 이터러블의 모든 Promise들을 병렬로 처리한다. 모든 Promise의 처리가 종료될 때까지 기다렸다가 처리 결과를 resolve나 reject하는 새로운 Promise를 반환한다. 모든 Promise가 성공이라면 각 Promise가 resolve한 결과를 배열에 담아 resolve하는 새로운 Promise를 반환한다. 이때 병렬 처리기 때문에 Promise의 처리가 완료되는 순서가 처음에 들어온 순서와 달라질 수 있지만, 결과로 반환되는 Promise는 Promise가 들어온 순서대로 결과를 배열에 담기 때문에 처리 순서가 보장된다. 반대로, 처리 도중 하나라도 실패한다면 가장 먼저 실패한 Promise가 reject한 에러를 reject하는 새로운 Promise를 즉시 반환한다.
만약 인자로 전달된 이터러블의 요소가 Promise가 아니면 Promise.resolve로 각 요소가 Promise로 감싸진다.

Promise.all([
  new Promise(/* 함수 */), 
  new Promise(/* 함수 */), 
  new Promise(/* 함수 */) 
]).then(console.log) // 배열
  .catch(console.log);

Promise.race는 모든 Promise를 병렬 처리하지 않고 가장 먼저 처리된 Promise가 resolve한 처리 결과를 resolve하는 새로운 Promise를 반환한다. 에러는 Promise.all과 마찬가지로 가장 먼저 실패한 Promise가 reject한 에러를 reject하는 새로운 Promise를 즉시 반환한다.

Promise.race([
  new Promise(/* 함수 */), 
  new Promise(/* 함수 */), 
  new Promise(/* 함수 */) 
]).then(console.log) // 가장 먼저 처리된 결과
  .catch(console.log);

출처

0개의 댓글