ivory's Log
그게 무엇이라도 항상 쉬운 일이다.

ivory's DevLog

[JavaScript] - Callback과 Promise

ivorycode 2020. 9. 18. 23:11
반응형

이 글은 JavaScript의 Callback과 Promise에 대해 적은 글입니다. 이 포스팅을 포함해 게시된 모든 포스팅의 큰 주제는 순차적인 흐름을 지키지 않습니다. 혼란에 주의해 주세요!


앞선 포스팅에 이어 JavaScript의 비동기 처리에 대해 좀 더 알아보자. JavaScript를 비동기 처리하려고 할 때 크게 3가지 방법이 있다. 바로 Callback 이용, Promise객체 이용, 마지막으로 async, await이 있는데, 그 전에 왜 JavaScript의 비동기적 처리가 필요한 이유부터 알아보자.

비동기적 처리가 필요한 이유?

JavaScript는 싱글 쓰레드 언어

JavaScript는 싱글 쓰레드(Thread) 언어다. 쓰레드란, 프로세스 내에서 실제로 작업을 수행하는 주체를 말한다. 쓰레드는 싱글 쓰레드, 멀티 쓰레드 2가지가 있는데 간단하게 차이점만 기억해보자. 싱글 쓰레드는 첫 번째 작업을 시작하고, 두 번째 작업을 하는 구조다. 즉, 순차적이다. 반대로 멀티 쓰레드의 경우엔 두 개의 쓰레드가 짧은 시간동안 두 개를 번갈아 가며 실행한다. 즉, 동시에 작업이 실행된다는 것이다.

서론이 길었는데, 결론은 JavaSciprt는 싱글 쓰레드 언어기 때문에 한 번에 하나의 작업만 실행되기 때문에 동시에 작업을 해야하는 경우엔 비동기적인 요청과 응답을 구현해야 한다.

 

Callback

콜백 지옥 이미지 출처: www.google.com 

Callback 함수란, 다른 함수의 매개변수(파라미터)로 함수를 전달하고, 어떤 이벤트가 발생한 후 매개변수로 전달한 함수가 다시 호출되는 것을 말한다. 더 간단히 하면, 어떤 작업을 다른 객체에게 시켜둔 다음 그 작업이 끝나는 것을 기다리지 않고 부를 때 까지 다른 일을 하는 것을 말한다.

 

하지만, Callback으로 비동기적인 작업을 처리하는 것은 굉장히 힘든 일이 될수도 있다. 위의 이미지만 봐도 대충 짐작이 간다. 흔히 Callback Hell 현상이라고 하는데, Callback 함수가 해당하는 결과 값을 가지고 다른 Callback 함수를 호출하고, 그 결과 값으로 또 다른 Callback 함수를 호출하는... 일종의 무한 루프를 돌게 되는 것이다. 특히 이런 Callback 함수를 익명으로 처리하는 경우가 많아서 눈으로 읽기조차 힘들 것이다. 물론 유지보수도 덩달아 힘들어진다. 말 그대로 Hell 그 자체!!

 

이런 지옥같은 현상을 해결하기 위해 몇 가지 방법들을 제시하고 있다.

 

- 익명함수(인라인 함수)에 이름을 붙여라.

- 코드를 간결하게 작성하자.

- 모듈화 하자.

- Promise를 사용하자.

 

위 3가지 방법은 가독성만을 개선하는 방법이기에 근본적인 해결법이 못된다. 하지만 Promise만은 다르다.

 

Promise

Promise는 어떤 값이 생성 되었을 때, 그 값을 대신하는 개념이다. 비동기 작업이 종료되고 난 후, 결과 값이나 오류를 처리할 수 있도록 연결하는 객체다. 결과나 오류 처리의 결과는 바로 알 수는 없고, 이 Promise라는 객체에 남겨두었다가 어떤 시점에서 결과를 사용할 수 있도록 한다. 한 마디로 다시 정리하면, JavaScript에서 비동기 함수 로직을 처리한 이후의 결과를 얻기 위한 함수이다.

 

Promise 로직 이미지 출처: www.google.com

Promise는 매개변수(파라미터)로 excutor라고 불리는 함수를 받고, 이 함수는 resolve와 reject function을 전달받는다. 또, Promise는 크게 3가지 상태가 있다.

 

- pending: 이행되거나 거부되지 않은 초기상태를 말한다.

- fulfilled: 연산이나 작업이 성공적으로 완료된 상태를 말한다.

- rejected: 연산이나 작업이 실패한 상태를 말한다.

 

위 3가지 중 하나의 상태를 가지며, 성공적으로 작업이 완료되면, resolve함수를 호출하고 오류가 발생하면 reject를 호출하는 것이다. excutor함수 연산 이후 resolve함수의 매개변수로 넘겨주는 함수를 then을 통해 받을 수 있고, 실패하면 catch를 통해 받을 수 있다.

 

참고 🙋‍♂️ 

- excutor는 결과를 최종적으로 만들어내는 제작 코드를 포함한다.

- executor에선 결과를 즉시 얻든, 늦게 얻든 상관없이 상황에 따라 인수로 넘겨준 콜백 중 하나를 반드시 호출해야 한다.

 

예제

 

let myFirstPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const num = Math.floor((Math.random() * 10) + 1);
    console.log("number is " + num);
    resolve(num);
  }, 250);
});
myFirstPromise.then(num => {
  if(num % 2 === 0) {
    console.log("Even!");
  } else {
    console.log("Odd...");
  }
});

 

new Promise를 생성하고 랜덤으로 숫자를 반환해주는 코드를 작성했다. 그리고 짝수면 Even, 홀수면 Odd를 반환해주는 방식이다. 하지만 이것을 아래와 같이 작성한다면 어떻게 될까? 우선 아래의 코드를 살펴보자.

 

setTimeout(function(){
  const num = Math.floor((Math.random() * 10) + 1);
  console.log(“number is “ + num);
}, 250);
if(num % 2 === 0) {
  console.log(“Success!”);
} else {
  console.log(“Fail…”);
}

 

이렇게 작성한다면 setTimout함수가 처리되기 전에 if안의 코드가 실행되어 num의 값을 읽지 못하게 될 것이다. 하지만 Promise를 적용하게 된다면 코드를 비동기적으로 처리할 수 있게 된다.

 

async와 await

async와 await를 사용하면 Promise를 더 쉽게 활용할 수 있다. 이 둘은 항상 함께 한다. await는 Promise 객체를 받아 처리하고, 비동기 작업이 아니고 동기적 작업이라면 리턴 값을 그대로 받는다. async는 Promise가 없으면 의미가 없는 함수다. Promise 객체를 통해 비동기적으로 처리된 작업을 동기적인 작업순서로 보여주는 역할을 하기 때문. 이제 작성법을 보며 이해해 보자.

 

async 예제

 

async function f() {
  return 1;
}

f().then(alert); // 1

 

async는 항상 function 앞에 위치한다. 이렇게 앞에 async를 붙이면 항상 Promise를 반환하게 된다. Promise가 아닌 값을 반환하더라도 resolved promise상태로 값을 감싸 값을 반환한다.

 

await는 async 함수 안에서만 작동한다. 아래의 문법을 참고해보자.

 

await 예제

 

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // 프라미스가 실행될 때까지 기다림 (*)

  alert(result); // "완료!"
}

f();

 

JavaScript는 await가 사용되면, Promise가 처리되기 전까지 기다리고 그 이후에 값을 반환한다. 위의 코드를 살펴보면, setTimeout 함수가 사용된 것을 확인할 수 있는데,  * 표시한 주석 부분에서 실행이 잠시 중단된다. 그 후 Promise 안에 있는 result의 값이 변수안에 할당되면서 1초뒤에 alert가 실행된다.

 

await는 사전적 의미로 "기다리다"라는 뜻을 가지고 있다. 말 그대로다. Promise가 처리되기 전까지 함수 실행을 중단시킨다. 그리고 그 이후에 실행된다.(말은 언제나 쉽다...)

 

참고 🙋‍♂️ 

- 일반 함수에는 await를 사용할 수 없다!! 반드시 함수 앞에 async를 사용해야한다.

- async함수 바깥의 최상위 레벨 코드에선 await를 사용할 수 없다.(하지만, 익명함수로 async를 감싼다면 사용할 수 있다.). 그렇기 때문에 관행처럼 .then/catch를 추가해 최종 결과나 처리되지 못한 에러를 다룬다.

반응형