본문 바로가기
Javascript

[JS] 프로미스와 콜백

by SeanK 2022. 3. 21.

 

오늘은 프로미스와 콜백에 대해 알아보자. 

 

개념은 이해하지만 막상 말로 설명을 하려니 막막한 감이 있으니 이번에야 말로 해당 개념을 완전히 정복해 보자!

 

아래의 블로그에서 상세히 설명되어 있어 번역해 옮겨본다. 

 

 출처: https://www.loginradius.com/blog/async/callback-vs-promises-vs-async-await/

 

Callback vs Promises vs Async Await

Learn fundamental concepts that JavaScript relies on to handle asynchronous operations. These concept includes Callbacks, Promises and the use of Async and Await to handle deferred operations.

www.loginradius.com

 

Callback vs Promises vs Async/Await

이번 블로그에서는 자바스크립트가 비동기적 처리를 진행하는 기본적 원리에 대해 설명하겠다. 즉 Callback 함수와 프로미스 그리고 Async/Await로 어떻게 미뤄진 코드 처리를 진행하는가에 대해 살펴본다.

 

이 세 개념을 비교하기 이전에, 동기적(blocking)과 비동기적(non-blocking)에 대한 간단한 이해부터 해보도록 하자.

 

동기와 비동기의 차이

쉽게 설명하기 위해 동기와 비동기의 차이를 설명할 수 있는 예를 살펴보자. 

 

식당에 갔다고 가정해보자. 웨이터가 식탁으로 와서 당신의 주문을 받고 이를 부엌에 전달한다. 그러면 주방장이 음식을 준비하는 동안 최대한 많은 테이블의 주문을 받기 위해 다른 테이블로 웨이터는 이동을 한다. 테이블은 이전의 테이블 음식이 완료될 때까지 기다려야 한다. 이러한 예가 바로 비동기 혹은 non-blocking 아키텍처이다. 여기서 웨이터는 마치 요청을 처리하는 스레드와 같다. 따라서 싱글 스레드가 여러 요청을 처리하기 위해 사용된다. 

 

non-blocking 혹은 비동기 아키텍쳐와는 반대로, blocking 혹은 동기적 아키텍처가 있다. 어떻게 작동하는지 한번 살펴보자. 다른 식당에 갔다고 가정해보자. 이 식당에서는 한 명의 웨이터가 당신에게 배정된다. 웨이터는 당신의 주문을 받고 이를 주방에 전달한다. 그리고 그 웨이터는 주방에 앉아 주방장이 당신의 음식이 완성되기 전까지 다른 주문은 받지 않고 기다리기만 한다. 이러한 방식이 동기적 혹은 blocking 아키텍처이다. 

 

첫 번째 식당의 예가 비동기적인 프로세스인 이유는 당신은 기다리지 않아도 웨이터가 이 테이블과 저 테이블 주문을 받기위해 돌아다니기 때문이다. 반면에 두 번째 식당은 자원(이 경우 웨이터)을 기다려야 했기 때문에 동기적인 프로세스이다. 이것이 바로 가장 근본적인 동기/비동기 차이다. 

 

한 가지 명심해야 할 부분은 싱글 스레드 이벤트 헨들링 시스템은 보통 이벤트 혹은 메시지 큐에 이용된다. 따라서 프로그램이 동기적으로 동작한다면 스레드는 첫 스테이트먼트가 완료될 때까지 기다리게 된다. 반면에 비동기 실행에서는 첫 스테이트먼트가 완료되기 이전에 다음 동작을 실행한다. 

 

비동기적 코드를 실행하는 방법이 몇 가지 있는데 그것이 바로 callback, promises, async/await이다. 

 

Callbacks:

자바스크립트에서 함수는 객체다. 따라서 객체를 함수에 파라미터로 넘길 수 있다. 

 

또한 함수를 다른 함수의 파라미터로 넘겨 외부함수의 내부에서 호출할 수도 있다. 따라서 콜백은 다름 함수에 넘겨지는 함수이다. 첫 번째 함수 실행이 완료되었을 때 두 번째 함수를 리턴하게 된다. 

 

아래는 콜백 함수의 예이다. 

function printString(){
   console.log("Tom"); 
   setTimeout(function()  { console.log("Jacob"); }, 300); 
  console.log("Mark")
}

printString();

만약 위 코드가 동기적으로 작동했다면 아래와 같은 결과가 나와야 할 것이다. 

Tom
Jacob
Mark

하지만 setTimeout 함수는 비동기 함수이기 떄문에 실제 아웃풋은 다음과 같다.

Tom
Mark
Jacob

자바스크립트엔 setTimeout이라는 내장함수가 있는데 이 함수는 특정 시간(밀리세컨드) 이후에 함수를 호출한다. 

 

즉, 메시지 함수는 이전에는 호출되지 않다가 특정 시간이 지나면 호출 되는 것이다. 따라서 콜백은 setTimeout의 argument로 넘겨지는 함수가 된다.

 

화살표 함수의 콜백:

선호에 따라 위와 동일한 함수를 새로운 자바스크립트의 함수 타입인 ES6의 화살표 함수로도 작성 가능하다. 

function printString(){
   console.log("Tom"); 
   setTimeout(() =>  { console.log("Jacob"); }, 300); 
  console.log("Mark")
}

printString();

아웃풋은 위와 동일하다. 

 

콜백의 문제점은 콜백헬이라고 불리는 문제를 만든다는 점이다. 기본적으로 외부 함수 안에 내부 함수를 계속해서 만들어내는 형식이기 때문에 가독성이 떨어지게 된다. 중첩되는 콜백 문제를 해결하기 위해 Promise가 나오게 되었다. 

 

Promises:

자바스크립트의 Promises는 실제 삶에서의 promise와 비슷하다. 이는 미래에 무언가를 할 것이라는 약속과 같다. 왜냐하면 약속은 미래에 실행할 것임을 보증하는 것이기 때문이다. 

 

프로미스는 두 가지 결과가 나올 수 있다: 시간이 되었을 때 약속을 지키거나 그렇지 못하거나.

 

이 또한 자바스크립트의 프로미스와 동일하다. 자바스크립트의 프로미스는 때가 되었을 때 실행이 되거나 혹은 거절당할 수 있다. 이는 IF 조건문과 비슷하게 들리지만 둘 사이에는 큰 차이점이 있다. 

 

프로미스는 실행의 비동기적 결과를 핸들링하기 위해 사용된다. 자바스크립트는 비동기적 코드블럭을 완전히 시행할 때까지 다른 동기적 코드를 기다리도록 설계되지 않았다. 프로미스로 비동기적 요청이 완료될 때까지 코드 블록의 실행을 연기할 수 있다. 이러한 방식으로 다른 코드의 실행이 간섭 없이 실행될 수 있다. 

 

프로미스의 상태:

 우선은 프로미는 객체다. 프로미스 객체에는 세 가지 상태가 있다. 

  • Pending: 프로미스가 성공하거나 실패하기 이전의 초기 단계
  • Resolved: 프로미스 완료
  • Rejected: 실패한 프로미스, 에러 발생

예를 들어 프로미스를 이용해 서버에 데이터를 요청함다면, 데이터를 받기 전까지 프로미스는 Pending 상태가 된다. 

 

만약 정보를 서버로부터 성공적으로 받으면 프로미스는 Resolved 상태가 된다. 만약 정보를 받지 못하면, 프로미스는 Reject 상태가 된다. 

 

프로미스 생성:

먼저 생성자를 이용해 프로미스 객체를 생성한다. 프로미스는 두개의 파라미터를 가지는데 하나는 success(resolve) 그리고 나머지는 fail(reject)를 위한 파라미터이다. 

const myPromise = new Promise((resolve, reject) => {  
    // condition
});

프로미스를 생성해보자:

const myFirstPromise = new Promise((resolve, reject) => { 
    const condition = true;   
    if(condition) {
         setTimeout(function(){
             resolve("Promise is resolved!"); // fulfilled
        }, 300);
    } else {    
        reject('Promise is rejected!');  
    }
});

위의 예제에서 프로미스에서 조건이 참이면 "Promise is resolved"를 리턴하며 프로미스는 resolve 되고 만약 참이 아니라면 "Promise is rejected" 에러를 반환한다. 이제 프로미스 객체를 생성했으니 사용을 해보자.

 

프로미스 사용:

프로미스 사용을 위해선 then()을 사용하고 거절되었을 경우는 catch()를 사용한다. 

myFirstPromise
.then((successMsg) => {
    console.log(successMsg);
})
.catch((errorMsg) => { 
    console.log(errorMsg);
});

조금더 나아가 보자.

const demoPromise= function() {
  myFirstPromise
  .then((successMsg) => {
      console.log("Success:" + successMsg);
  })
  .catch((errorMsg) => { 
      console.log("Error:" + errorMsg);
  })
}

demoPromise();

만약 프로미스의 조건이 참이면 drmoPromise() 호출하고 콘솔에는 아래와 같이 기록될 것이다. 

Success: Promise is resolved!

만약 프로미스가 rejected 된다면 catch() 메서드로 바로 건너뛰어 다른 메세지를 보게 된다. 

Error: Promise is rejected!

 

체이닝이란 무엇인가?

떄떄로 많은 수의 비동기 요청을 시도해야 할 경우가 있다. 이때 체이닝이라고 불리는 메서드를 붙여 넣음으로 서 하나의 프로미스가 resolve 되면 새로운 프로미스를 실행하도록 할 수 있다. 

 

또 다른 프로미스를 만들어보자:

const helloPromise  = function() {
  return new Promise(function(resolve, reject) {
    const message = `Hi, How are you!`;

    resolve(message)
  });
}

위 프로미스를 myFirstPromis에 아래와 같이 체이닝 할 수 있다. 

const demoPromise= function() {

  myFirstPromise
  .then(helloPromise)
  .then((successMsg) => {
      console.log("Success:" + successMsg);
  })
  .catch((errorMsg) => { 
      console.log("Error:" + errorMsg);
  })
}

demoPromise();

조건이 참이기 떄문에 아웃컴은 아래와 같다. 

Hi, How are you!

hello 프로미스가 .then으로 체이닝 되면 하위. then은 이전 프로미스의 데이터를 사용하게 된다. 

 

Async/Await:

 Await은 기본적으로 프로미스의 문법적인 설탕과 같다. 이는 비동기적 코드를 더욱 개발자가 이해하기 쉬운 동기적/절차적 코드처럼 보이도록 만든다.

 

Async/Await 문법:

async function printMyAsync(){
  await printString("one")
  await printString("two")
  await printString("three")
}

위 예제에서 함수 printMyAsync()에 async 키워드를 사용한 것을 확인할 수 있다. 이것은 자바스크립트에게 우리가 async/await 문법을 사용할 것임을 알려주어 await을 사용할 수 있도록 하기 위함이다. 이 뜻은 Await은 글로벌 단계에서 사용할 수 없음을 의미한다. 이것은 항상 감싸주는 함수를 필요로 한다. 혹은 await은 async 함수에서만 사용할 수 있다고도 말할 수 있다. 

 

await 키워드는 동기적 함수안에서 모든 비동기 프로미스가 동기적으로 작동하도록 한다. 즉, 서로가 끝날 때까지 기다린다. await은 then()과 catch() 메서드의 콜백을 제거한다. async/await에서 async는 프로미스를 리턴할 때 붙여지고 await은 프로미스를 호출할 때 붙여진다. try와 catch도 거절된 비동기 값을 얻기 위해 사용된다. 

 

Async와 Await을 이해하기 위해 예제를 살펴보자.

async function demoPromise() {
  try {
    let message = await myFirstPromise;
    let message  = await helloPromise();
    console.log(message);

  }catch((error) => { 
      console.log("Error:" + error.message);
  })
}

// finally, call our async function

(async () => { 
  await myDate();
})();