210627. Today I Learned(TIL) : 비동기(Asynchronous) Promise & Async Await

syong·2021년 6월 27일
0

TIL

목록 보기
17/32

Callback

Promise와 Async Await을 다루기 전에 먼저 callback으로 비동기 호출하는 구조를 살펴보자.

콜백은 말 그대로 회신, 답신이라는 의미이다. 따라서 콜백함수는 함수의 인자로 넘겨주었을 때, 해당 함수를 실행하면 내부에서 같이 실행되는 함수이다. 인자로 넘겨준 함수가 대답을 돌려준다는 의미에서 콜백이라 칭하는 것이다.

콜백을 이용하여 비동기 호출을 하게 되면 다음 과 같은 코드를 작성할 수 있다.

const printString = (string, callback) => {
  setTimeout(
    () => {
      console.log(string);
      callback();
     },
    1000
   )
}

const printAll = () => {
  printString('A', () => {
    printString('B', () => {
      printString('C', () => {})
    })
  })
}

printAll() // expected: A B C

위와 같이 코드를 작성하면 콜백함수가 콜백함수를 부르고 또 그 콜백함수는 또 다른 콜백함수를 부르게 되면서 먼저 부른 콜백함수의 결과부터 차례로 출력된다. 하지만 위 코드를 조금만 자세하게 뜯어보아도 이런 구조에는 아래와 같은 치명적인 단점이 있다는 것을 알 수 있다.
1. 가독성이 떨어진다.
2. 어떤 콜백함수가 어디에서 불리는지 찾기 힘들기 때문에 코드의 유지 및 보수가 까다롭다.

이런 구조를 '콜백 지옥(Callback Hell)'이라고 하는데, 콜백함수가 마치 지옥의 구덩이로 빠지는 것처럼 끝없이 안쪽으로 파고드는 모습 때문에 붙여진 이름이다. 이 콜백 지옥을 보완하기 위해 앞서 다루었던 Promise를 사용하여 코드를 작성해보자.

Promise

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        console.log(string);
        resolve();
      },
      1000
     )
  })
}

const printAll = () => {
  printString('A')
  .then(() => {
    return printString('B')
  })
  .then(() => {
    return printString('C')
  })
}

printAll() // expected: A B C

프로미스를 이용하면 함수에 콜백 인자를 받지 않고, 함수는 새로운 프로미스 인스턴스 객체를 리턴하게 된다.
대신 프로미스 만의 콜백함수가 있는데, 이 콜백함수의 인자로는 resolve와 reject 두 개가 있다. resolve는 프로미스가 예정대로 잘 실행이 되면 호출되는 프로미스 내장 함수이다. 반대로 reject는 에러를 핸들링할 때 호출되는 함수이다. 프로미스는 .then으로 여러개를 이어붙일 수 있는데, 연속적으로 콜백을 호출하는 방법보다 훨씬 가독성이 좋아지고, 비교적 평평한 모양의 코드를 작성할 수 있게 된다. 또한 프로미스 체이닝의 맨 마지막에 .catch를 사용하면 프로미스 체이닝이 끝난 후 마지막에 에러를 잡아서 핸들링할 수 있게 도와준다. 예시 코드는 다음과 같다.

const printAll = () => {
  printString('A')
  .then(() => {
    return printString('B')
  })
  .then(() => {
    return printString('C')
  })
  .catch(() => {
    console.log('error')
  )}
}

하지만 콜백 지옥을 벗어나기 위해 사용하기 시작한 프로미스도 '프로미스 지옥'을 만들어낼 수 있다. 따라서 모든 비동기 함수를 1차원의 평평한 코드로 작성할 수 있도록 만들어주는 async await를 추가로 사용하게 되었다.

Async Await

async await은 굉장히 간단하다. 함수 앞에 async 키워드가 붙고 콜백 함수 앞에 await 키워드를 붙여주면 해당 콜백 함수의 실행 결과값이 계산되어 나오고, 그 결과값을 변수에 저장하여 실행시킬 순서대로 평평하게 마치 1차원 동기함수 실행처럼 작성하면 우리가 원하는 대로 비동기적 함수 실행이 이루어진다. 코드로 표현하면 다음과 같다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        console.log(string);
        resolve();
      },
      1000
     )
  })
}

const printAll = async () => {
  const first = await printString('A');
  console.log(first);
  
  const second = await printString('B');
  console.log(second);
  
  const third = await printString('C');
  console.log(third);
}

printAll() // expected: A B C

위와 같이 작성하면 1차원 함수로 코드를 작성할 수 있어서 읽기 편하고 코드도 훨씬 간결하고 깔끔해진다. 간단하고 짧은 코드라고 해서 매번 async await을 사용할 필요는 없다. 연결할 프로미스가 너무 많아 프로미스 헬을 만들어낼 우려가 있는 상황에 적절하게 사용하면 순식간에 정돈된 코드를 작성할 수 있다.

0개의 댓글