JS 공식문서 스터디 8-2. Promise

CHO WanGi·7일 전
3

Javascript

목록 보기
20/20

Promise?

JS의 비동기 처리 위한 패턴으로 콜백함수를 사용했음,
그러나 콜백 지옥으로 인해 가독성이 떨어지고, 에러 처리가 곤란하다는 문제점이 발생.

Callback 의 문제점

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.error(`${xhr.status} ${xhr.statusText}`)
   }
 }
}

get(`~~~~`);

해당 get 함수가 응답 결과를 반환하는 과정을 살펴볼 필요가 있다.
get 함수는 일단 비동기 함수.
1. xhr.open, xhr.send 까지 실행되어도, 브라우저는 요청을 비동기적으로 서버로 보냄.
2. 그순간 Get 함수는 이미 실행을 끝내고 반환(return)
3. 나중에 서버에서 응답이 오면, xhr.onload 콜백이 실행

따라서 console.log는 찍히지만, get함수 자체는 아무것도 반환 X

즉,

xhr.onload = () => {
  return JSON.parse(xhr.response); // ❌ get() 함수 호출자가 이 값을 못 받음
}

onload 안에서 return을 써도, 함수의 반환값이 되지 않는다는 뜻.

Let's Deep Dive

  1. get 함수 실행시점
    get을 호출 -> xhr.open, xhr.send가 실행
    xhr.onload = () => {...} 이 이벤트 핸들러로 등록만 진행
    아직 서버 응답이 안왔기에 핸들러 실행 X

  2. Call Stack 처리
    JS는 싱글 스레드라서 한줄씩 호출 스택에 넣고 실행하고 뺌
    console.log는 동기 처리라 바로 스텍에 들어와서 실행
    그러나 xhr.onload는 비동기라 스택에 안들어오고 나중에 실행할 함수로만 등록

  3. 서버 응답 도착시
    브라우저, load 이벤트발생
    xhr.onload 콜백이 Task Queue에 들어감
    Event Loop가 호출 스택이 빌때까지 기다렸다가 콜백을 스택에 올려 실행

이때 저 비동기함수가 또 비동기를 호출한다면 어떻게 될까?

// 1. post 정보 가져오기
get(`${url}/posts/1`, ({ userId }) => {
  console.log(userId); // 1

  // 2. post 안의 userId로 user 정보 가져오기
  get(`${url}/users/${userId}`, userInfo => {
    console.log(userInfo); 
    // {id: 1, name: "Leanne Graham", username: "Bret", ...}
  });
});

이런식으로 첫번재의 get 요청이 끝나야 userId를 알수 있으므로
두번째 POST 요청은 비동기처리임에도 get 요청이 끝날때까지 기다리게 된다.
결국 콜백 함수 호출이 중첩되어 복잡도가 높아지는데, 이를 "Callback Hell" 이라고 한다.

또한 에러처리 부분에서도 한계점을 보여주는데,
이를 해결하는 방법이 바로 Promise

Promise

프로미스의 생성

new 연산자 + Promise 생성자 함수 조합으로 생성.
Promise 생성자 함수는 비동기처리를 수행할 resolve 와 reject 함수를 Parameter로 받음.

  • Promise
const promise = new Promise((resolve, reject) => {
  // 여기 안에서 비동기 작업 수행
  if (/* 비동기 처리 성공 */) {
    resolve('result');   // 성공 시 호출(fulfilled)
  } else {
    reject('failure reason'); // 실패 시 호출(rejected)
  }
});

따라서 위의 get 함수를 Promise를 이용해서 바꾸어보면

const get = url => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.send();

    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.response));
      } else {
        reject(new Error(`${xhr.status} ${xhr.statusText}`));
      }
    };
  });
};

get('~~~~')
  .then(data => console.log(data))
  .catch(err => console.error(err));

프로미스의 상태

생성 직후 프로미스는 기본적으로 Pending.
이 비동기 처리가 성공하면 resolve 함수 호출하여 프로미스를 fulfilled 상태로 변경
반면 비동기 처리가 실패하면 reject 함수를 호출하여 프로미스를 rejected 상태로 변경

이런식으로 비동기 처리가 성공하면 [[PromiseState]]: "fulfilled"로 변경된다.
에러면 rejected.

프로미스의 후속 처리 메서드

프로미스의 상태가 변화하면 이에 맞게 후속처리를 해야한다.
fufilled 가 되면 처리 결과를 갖고 무언가를 해야하고,
rejected 상태가 되면 처리 결과(에러)를 갖고 에러 처리를 해야한다.

Promise.protype.then , Promise.prototype.catch

then은 두개의 콜백 함수를 인수로 전달 받는다.

  1. 첫번째 콜백함수는 fulfilled 상태 일때(resolve 함수가 호출된 상태) 호출, 비동기 처리 결과를 인수로 전달
  2. 두번째 콜백함수는 rejected 상태 일때(reject 함수가 호출), 이때 콜백 함수는 프로미스 에러를 인수로 전달

catch는 한개의 콜백함수만 인수로 전달받는데, 프로미스가 rejected 상태인 경우만 호출된다.

new Promise(resolve => resolve('fulfilled'))
  .then(v => console.log(v))
  .catch(e => console.error(e));
// 출력: fulfilled

new Promise((_, reject) => reject(new Error('rejected')))
  .then(v => console.log(v))
  .catch(e => console.error(e));
// 출력: Error: rejected

따라서 콜백에서 번거로웠던 에러처리를 then ... catch를 활용하여 더 간단하게 처리 가능하다.

  • Promise.prototype.finally
    프로미스의 성공과 실패 여부에 상관 없이 무조건 한번 호출.
    프로미스의 상태와 상관 없이 무언가 수행해야할때 유용하다.

Promise 체이닝

const url = 'https://jsonplaceholder.typicode.com';

// id가 1인 post의 userId를 획득
promiseGet(`${url}/posts/1`)
  // 그 userId를 이용해 user 정보 요청
  .then(({ userId }) => promiseGet(`${url}/users/${userId}`))
  // 최종적으로 user 정보 출력
  .then(userInfo => console.log(userInfo))
  // 에러 발생 시 처리
  .catch(err => console.error(err));

then -> then -> catch 순으로 후속처리 메서드를 호출,

Promise.all

여러개의 비동기 처리를 모두 병렬로 처리시 사용

const requestData1 = () => new Promise(r => setTimeout(() => r(1), 3000));
const requestData2 = () => new Promise(r => setTimeout(() => r(2), 2000));
const requestData3 = () => new Promise(r => setTimeout(() => r(3), 1000));

Promise.all([requestData1(), requestData2(), requestData3()])
  .then(res => {
    console.log(res); // [1, 2, 3]  → 약 3초 소요
  })
  .catch(console.error);

3개의 프로미스를 병렬로 시작, 총 3초가 걸린다.
결과 배열 순서는 시작 순서를 보장한다.

async/await

Promise 를 활용한 콜백패턴이 가독성이 좋지 않다보니 등장한 개념

async function runParallel() {
  const p1 = requestData1();
  const p2 = requestData2();
  const p3 = requestData3();
  const res = await Promise.all([p1, p2, p3]);
  console.log(res); // [1, 2, 3] → 약 3초
}
runParallel();

위와 동일한 기능을 한다.

Promise.race

all 처럼 이터러블을 인수로 받는데, all과 다른점은 가장 먼저 끝난 프로미스의 결과만 반환한다.
이름처럼 race 1등만 반환

Promise.race([
  new Promise(r => setTimeout(() => r(1), 3000)),
  new Promise(r => setTimeout(() => r(2), 2000)),
  new Promise(r => setTimeout(() => r(3), 1000))
]).then(console.log); // 3 (약 1초 뒤)

마이크로태스크 큐

setTimeout(() => console.log(1), 0);

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));

JS에서 Queue는 두종류인데

  • MicroTask Queue : Promise.then/catch/fianlly 가 여기 들어감
  • Task Queue setTimeOut/setInterval, DOM 이벤트가 여기 들어감

근데 이벤트 루프의 우선순위가
1. Call Stack이 비면
2. 마이크로 태스크 큐를 먼저 전부 비운다
3. 그 다음 태스크 큐에서 1개 가져와서 실행, 다시 2번으로 돌아가 반복

따라서 마이크로 태스크큐에 들어간 프로미스가 먼저 실행되고, 그다음 태스크큐로 이동하기에
위 출력은 2 -> 3 -> 1 이 되는 것.

fetch

fetch는 XMLHttpRequest 객체와 마찬가지로
HTTP 요청 전송 기능을 제공하는 클라이언트 사이드 Web API

이 fetch 함수가 Response 객체를 래핑한 Promise 객체를 반환

단, 404 Found Err, 500 Internal Server Err 같은 HTTP 에러가 발생해도,
에러를 reject 하지 않고, 이 Response 객체를 resolve 한다는 것 기억하자.

오프라인 등 네트워크 장애나 CORS 에러에 의해 요청이 미완료 된 경우에만 프로미스를 reject

profile
제 Velog에 오신 모든 분들이 작더라도 인사이트를 얻어가셨으면 좋겠습니다 :)

0개의 댓글