Javascript Promise 이해하기

고광필·2022년 2월 17일
0

Front

목록 보기
4/33

콜스택과 이벤트 루프 포스팅에서 단일 스레드언어인 자바스크립트에서 어떻게 비동기를 처리하는지 공부했습니다.

비동기는 실제 코드로 어떻게 처리하는지 공부한걸 기록하겠습니다.

초기 비동기 코드

문제점

console.log('start');

setTimeout(() => {
  console.log('loading')
}, 0);

console.log('end');

setTimeout()을 이용한 비동기 코드입니다.
0초 후에 loading을 찍는 코드지만, 비동기 코드는 태스크 큐로 이동되어 나중에 실행됩니다.
따라서 start, end, loading의 결과가 나옵니다.

위 코드는 초기에 사용된 비동기 코드의 문제점을 보여주고 있습니다.
비동기가 특정 로직을 기다리지 않고 다음 코드를 실행하기 때문에 원하는 타이밍에 동작을 진행할 수 없습니다.

callback함수로 해결

특정 로직 다음에 원하는 코드를 실행할 수 있는 방법으로 callback함수 사용이 있습니다.

function printStart() {
  console.log('start');
}

function printLoading(callbackFn) {
  setTimeout(() => {
    console.log('loading')
    callbackFn();
  }, 0);
}

function printEnd() {
  console.log('end');
}

printStart();
printLoading(printEnd);

위 코드는 비동기인 printLoading이 완료된 후에 실행될 printEnd를 callback함수로 넘기는 코드입니다.
비동기 코드가 가지는 문제점을 해결한 코드이기도 합니다.

callback함수의 문제점

비동기 코드에서 이 코드 다음 저 코드, 저 코드 다음, 그 코드... 이런식으로 끊임 없이 계속 붙어서 깊은 depth를 가지고 있어 알아보기 힘든 코드를 콜백지옥에 빠진 코드 라고 합니다.

읽기가 힘들뿐더러, 중간에 비동기 요청이 오래 걸리면 그만큼 전체 동작도 멈추는 셈입니다.
읽기 힘들다는 뜻은 정확하게는 비동기 callback의 경우 결과값을 바로 받아야 하기 때문에 코드가 지저분해졌다는 뜻입니다.

ES6 Promise

배경

  1. Promise가 생기기 전 자바스크립트 라이브러리마다 비동기를 다루는 로직이 조금씩 달랐습니다.
    개발자는 사용하는 라이브러리에 따라서 어떻게 비동기를 처리하는지 내부를 뜯어봐야 했습니다.
    굉장히 피곤한 상황인데 이런 문제를 해결하기 위해 Promise가 탄생합니다.

  2. callback함수의 문제점에서 살펴본것처럼 비동기 처리는 응답 시 (성공, 실패) 실행할 콜백 함수를 파라미터로 입력받아야 합니다.

Promise는 이런 상황을 Promise로 감싸주는것으로
비동기 처리 로직의 인터페이스를 매개변수를 받지 않고, Promise를 반환하는것으로 통일시킵니다.
또한 매개변수를 받지 않는다는 뜻은 요청과 처리로직의 분리가 가능함을 뜻합니다.

3가지 상태

  1. Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  2. Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  3. Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

기본 사용

const promise = new Promise((resolve, reject) => {
  // 비동기 코드 로직
  // 오류 시 reject()
  // 완료 시 resolve()
})

비동기 코드 로직이 오류시 reject()를, 완료시 resolve()를 사용하면 해당 값이 반환된다.

장점

// 이렇게도 되고
const response = 비동기().then((data) => ...);

// 아래 코드는 요청과 처리 로직의 분리
const response = 비동기(); // 요청
// ...
// 이것 저것 다른 코드
// ...
response.then((data) => ...); // 처리

비동기 처리 인터페이스를 통일했으니 편리하다는 장점 외에도
Promise 배경에서 매개변수를 받지 않는다는 뜻은 요청과 처리로직의 분리가 가능하다는 뜻과 같다고 했습니다.

위 코드는 동일한 내용이지만 기존의 callback 함수를 사용할 때와는 다르게 요청과 처리를 분리해서 사용할 수 있음을 보여줍니다.

.then() .catch() 비동기 처리

import React, { useEffect } from "react";
import axios from "axios";

function Test_Then() {
  axios
    .get("https://jsonplaceholder.typicode.com/users")
    .then((response) => {
      // throw new Error("에러 발생!");
      console.log(response);
      console.log(response.data);
    })
    .catch((error) => console.log(error));
}

const App = () => {
  useEffect(() => {
    Test_Then();
  }, []);

  return <>App</>;
};

export default App;


react에서 간단하게 jsonplaceholder 사이트 데이터를 가져오는 코드입니다.
위 비동기 코드는 then catch를 통해 성공 / 실패 시 처리를 하고 있습니다.

.then() .catch() 문제점

그러나 이 then catch도 문제점이 있습니다.
A요청 이후 B요청 이후 C요청...과 같이 .then() 체이닝이 반복되면 콜백지옥에 빠진것과 같은 코드를 마주하게 됩니다.

A.then().then().then().catch()....

then catch 이후에 새롭게 나온 방식이 async await 입니다.

async await 비동기 처리

import React, { useEffect } from "react";
import axios from "axios";

async function Test() {
  try {
    const response = await axios.get(
      "https://jsonplaceholder.typicode.com/users"
    );
    // 성공시 로직
    console.log(response);
    console.log(response.data);
  } catch (error) {
    // 에러시 로직
    console.log(error);
  }
}

const App = () => {
  useEffect(() => {
    Test();
  }, []);

  return <>App</>;
};

then catch를 사용하던 코드를 async await로 바꿨습니다.
async await 키워드를 사용하며, try catch를 통해 error를 처리합니다.

콜백지옥을 겪을 수 있는 코드가 async await를 이용하면

try {
  const A = await 요청1();
  A를 이렇게 저렇게
  const B = await 요청2();
  B를 이렇게 저렇게
  const C = await 요청3();
  C를 이렇게 저렇게
} catch (error) {
...
}

와 같이 더 깔끔해짐을 알 수 있습니다.
또한 순서와 await 키워드가 붙은 곳이 비동기임을 직관적으로 알 수 있습니다.

단점

하지만 이 방법에도 단점이 존재합니다.

try {
  const A = await 요청1();
  A를 이렇게 저렇게
  
  // 이 이후로 A는 사용하지 않지만, A에 접근할 수 있다
  const B = await 요청2();
  B를 이렇게 저렇게
  const C = await 요청3();
  C를 이렇게 저렇게
  // console.log(A) // 접근 가능
} catch (error) {
...
}

위 코드에서 const B 부분부터 요청1에 대한 응답 변수A는 사용하지 않습니다.
하지만 계속해서 접근이 가능합니다.

요청1
  .then(A => {
    A를 이렇게하고 요청2
  })
  .then(B => {
    B를 이렇게하고 요청3
    // console.log(A) // 오류
  })...

then catch 코드에서는 A를 사용하는 블록에서만 접근이 가능하지만
async await는 A를 사용하지 않는 코드에서도 접근이 가능하게 됩니다.

Promise 내장함수

  1. Promise.all()
    모든 프로미스가 완료되거나, 한 프로미스가 거부될때까지 대기하는 새로운 프로미스를 반환합니다.
  2. Promise.race()
    모든 프로미스 중 하나라도 resolve, 또는 reject 되면 종료합니다.
  3. Promise.any()
    모든 프로미스 중 하나라도 resolve되면 종료합니다.
  4. Promise.allSettled
    모든 프로미스들의 결과 유무를 배열로 반환합니다. (성공 / 실패 포함)

정리

비동기 처리
=> 비동기 콜백
=> Promise 탄생
=> .then() .catch()
=> async await

관련 개념들을 찾아보면서 왜 해당 개념이 나온건지, 어떤 점이 좋아진건지, 어떻게 사용하는지를 공부했습니다.

비동기 처리 성공 / 실패 유무와 상관없이 돌아가는 로직의 경우 finally 키워드를 이용할 수 있습니다.

참고

ZeroChoTV Youtube - [인간 JS 엔진 되기 2-1]프로미스의 최고 장점을 아십니까
teahoon Youtube - async await, 정말 좋은데... 이게 왜 좋은걸까요?
캡틴판교 - 자바스크립트 비동기 처리와 콜백 함수
javascript.info - async와 await

수정

220406 데브코스 수업 이후 전체적으로 내용 보충 및 수정했습니다
220915 코드 마크다운에 js, jsx를 추가했습니다

profile
이해하는 개발자를 희망하는 고광필입니다.

0개의 댓글