콜백과 Promise

Yeom Jae Seon·2021년 3월 3일
0

JavaScript

목록 보기
6/6
post-thumbnail

비동기


비동기란 무엇일까요?

비동기를 알기전에 동기부터알아봅시다.

동기란 코드가 순서대로 진행되는 것을 의미합니다.!

console.log('1');
console.log('2');
console.log('3');

결과는 1이 콘솔에 찍히고 그다음 2 그다음 3이 찍히겠죠?

이게 동기입니다.

비동기란 동기와는 반대로 코드가 순서대로 진행되지 않는걸 의미합니다.
그의미는 곧 기다리지 않는다라고 생각해도 됩니다.

만약 앞의 코드가 100초가걸리면 기다리지않고 그다음 코드로 넘어가버리는걸 의미합니다.

예를들어볼게요!

console.log("1");
setTimeout(() => {
  console.log("2");
}, 3000);
console.log("3");

이 코드는 어떤식으로 동작할까요? 위와 똑같이 동작할까요?

그렇지않습니다.!

1 -> 3 -> 2로 콘솔에 찍히게 됩니다.

이렇게 무언가 시간이걸리는 작업에대해선 자바스크립트는 기다리지 않고 다음 코드로 넘어가버립니다.
setTimeout(() => { console.log("2"); }, 3000);
이 코드는 시간이 걸리는 작업이라서 기다리지않고 바로 console.log('3')으로 넘어갔습니다!

보다 더 자세한 이유는 자바스크립트 동작 방식과 관련이있습니다.
call stack, web apis, taskqueue, event loop... 이러한 용어에 대해서 공부하시면서 보면 좋을거같아요 ! 여기선 다루진 않고 그냥 자바스크립트는 동작 방식 때문에 비동기적으로 동작한다고 알고 넘어갈게요!

그러면 무조건 비동기적으로 동작하는 코드들은 마지막에 동작하게 될까요? 그럼 비동기적으로 받아온 데이터에 대해선 어떻게 처리하죠?

예를 들어볼게요

유튜브 api를 이용해서 제가 좋아하는 파카 영상을 받아오고 싶어요
(여기서 유튜브 api를 이용해서 받아오는 과정은 시간이걸리는 작업이 겠죠? 비동기적으로 동작합니다.)
파카 영상을 받아오고 잘받아왔나 콘솔에 찍어보고싶네요 .

어떻게할까요?

콜백함수를 이용하면 됩니다.!!

콜백


콜백함수는 뭘까요?
call back, 나중에 부른다는 의미이죠?

보다더 정확한 의미는 함수의 인자에 전달되는 함수를 의미합니다.
자바스크립트의 함수는 인자로 함수를 전달할수도 있습니다!

이를 이용해서 비동기적으로 동작하는 코드가 다 실행되면 콜백함수를 이용해서 추가적인 작업이 가능합니다.

예시를 볼까요?

let youtube;

function getYoutubeChannel() {
  setTimeout(() => {
    youtube = "PAKA";
  }, 3000);
}

getYoutubeChannel();
console.log(youtube);

youtube라는 변수를 먼저 만들고 getYoutubeChannel()이라는 함수를 이용해서 youtube변수에 'PAKA'를 넣고싶네요.
그리고 콘솔에 찍어보고싶어요
(setTimeout을 유튜브 api를 받아오는데 3초걸린다고 생각하시면서 보시면 더 이해가 잘될거 같네요!)

파카 영상을 잘받아와서 콘솔에 찍혔을까요?

결과는 undefined이네요.

이유는 당연합니다. 위의 콘솔 예시와도 같은 이유입니다.

함수가 호출되긴했는데 3초가걸리죠?
그래서 기다리지않고 console.log(youtube)를 먼저 실행해버린겁니다.
youtube변수에는 아무것도 할당하지 않았으니 undefined가 찍히는 거구요.

그럼 어떻게 하면 정상적으로 youtube변수에 값을 제대로 할당할수 있을까요?

콜백함수를 이용해 봅시다

let youtube;

function getYoutubeChannel(callback) {
  setTimeout(() => {
    callback("PAKA");
  }, 3000);
}

getYoutubeChannel((channelName) => {
  youtube = channelName;
  console.log(youtube);
});

변경된게 눈에 들어오시나요?
천천히 보면서 손으로 따라치면서 익히면 어렵지 않을거에요

원하는 데로 됐네요!

이렇게 콜백함수를 이용해서 getYoutubeChannel함수를 호출하며 인자로 함수를 전달했습니다. 여기선 익명의 화살표함수를 이용했어요.
그렇게 전달된 익명의 화살표함수가 getYoutubeChannel함수의 인자 callback으로 전달받아 3초뒤에 callback함수의 인자에 PAKA를 전달해서 channelName에 라는 인자에 PAKA가 잘받아와서 변수 youtube에 할당이된뒤 콘솔에 찍히는 겁니다.

과정이 조금 복잡하네요. 어려운 부분이라 생각합니다.

아무튼 이런식으로 비동기적으로 동작하는 코드를 콜백함수를 이용해서 처리를 할수가 있어요
이런방식을 콜백 기반 비동기 프로그래밍이라 부릅니다.

그런데 파카영상이 너무재밌어서 3초 있다가 또 파카영상을 유튜브 api를 통해서 받아와서 보고 본횟수를 기록하고 싶군요!
(영상은 한번보는데 3초걸린다고 가정합니다!)
어떻게 해야할까요?

콜백함수를 통해 비동기적으로 처리가 완료된 이후 실행할수 있다 했죠?
콜백함수안에서 다시 getYoutubeChannel함수를 호출하면 어떨까요?

let youtube;

let count = 0;

function getYoutubeChannel(callback) {
  setTimeout(() => {
    ++count;
    callback(`PAKA ${count}번봄`);
  }, 3000);
}

getYoutubeChannel((channelName) => {
  youtube = channelName;
  console.log(youtube);
  getYoutubeChannel((channelName) => {
    youtube = channelName;
    console.log(youtube);
  });
});

(count변수를 추가했습니다.!)

3초있다가 PAKA 1번봄이 콘솔에 찍히고 또 3초있다가 PAKA 2번봄이 콘솔에 찍히게 되네요

즉 6초가 걸리네요

이런식으로 콜백함수 안에서 다시 함수를 호출해서 첫번째 콜백함수가 실행된 뒤 다시 setTimeout 3초를 실행해서 순차적으로 영상을 볼수 있게 됐네요

근데 파카영상이 너무재밌어서 연속으로 5번보고싶어요.
그럼 어떻게 될까요?

...

getYoutubeChannel((channelName) => {
  youtube = channelName;
  console.log(youtube);
  getYoutubeChannel((channelName) => {
    youtube = channelName;
    console.log(youtube);
    getYoutubeChannel((channelName) => {
      youtube = channelName;
      console.log(youtube);
      getYoutubeChannel((channelName) => {
        youtube = channelName;
        console.log(youtube);
        getYoutubeChannel((channelName) => {
          youtube = channelName;
          console.log(youtube);
        });
      });
    });
  });
});

헉 코드가 옆으로 길어지는게 보이시나요?
영상을 받아와서 3초동안 보고 또 다시 영상을 받아와서 3초동안보고 또다시 영상을...
이렇게 총 5번 실행되서 5번 영상을 받아와서 15초동안 영상을 보게 되었지만 코드는 굉장히 복잡해졌네요

콜백안에 인자로 함수를 전달하는, 콜백함수를 가지고있는 함수를 호출하고 또 그 함수안에 콜백함수를 전달하는 함수를 호출..

이렇게 되면 중간에 9초쯤에 영상이 못받아와지거나 여러 문제가 생겼을 때 해결이 복잡할거같네요..

엄청 복잡하진 않지만 더 복잡해진다면 더 지옥에 빠질거같아요

이게바로 콜백 지옥입니다.

비동기적으로 동작하는 코드에 대해서 콜백함수를 통해서 잘 처리했는데 순차적으로 여러 번 콜백함수로 비동기처리를 하려할 때 이런 문제점이 생기네요

어떻게 처리해야할까요?

Promise


자바스크립트에는 Promise라는 객체가 존재합니다.
이 Promise를 통해서 콜백지옥에서 헤어나올수 있습니다!!

Promise부터 알아볼까요?

const iLovePromise = new Promise((resolve, reject) => {
  resolve("결과!!");
});

먼저 프라미스는 이런식으로 만들수 있습니다.
조금 복잡해보이지만 저희는 이미 존재하는 걸 사용하면 되기 때문에 어렵지 만은 않습니다!!

Promise에 함수가 전달되는데 이함수를 executor, 한국어론 실행함수라고 합니다. 이 함수는 Promise가 생성될때 자동으로 실행됩니다!

그럼 executor내부를 보면 인자로 resolvereject가 있네요 이건 콜백함수입니다. 즉 resolvereject는 함수입니다. 인자로 전달된 함수를 콜백함수라고 한다했죠?
resolvereject도 그렇습니다.

어떻게 동작하나 보면

executor에서는 위 두 콜백중 하나를 반드시 호출해야합니다.

  • resolve(value) : 일이 성공적으로 끝난경우 결과를 value와 함꼐 호출합니다.
  • reject(error) : 에러가 발생했을 때 에러 객체를 나타내는 error과 함꼐 호출합니다.

정리하면 executor는 자동으로 실행되는데, 여기서 저희가 처리하고싶은 일을 처리할수 있습니다. 처리가 끝나면 처리 성공여부에 따라서 resolve, reject중 하나를 호출합니다.(둘다 호출할순 없습니다!)

이에 따라서 Promise는 세가지의 상태를 갖습니다.

  • Pending : 대기중
  • Fulfilled : 이행됨
  • Rejected : 거부됨

만약 일을 처리하는데 3초가걸리고 3초뒤에 처리 성공여부를 알수 있다고 합시다.
그러면 3초동안 기다리는 시간을 대기중인 Pending상태라하고 이후의 결과에 따라서 성공하면 Fulfilled상태이며 resolve(value)를 호출합니다.
만약 실패한다면 Rehected상태이며 reject(error)를 호출합니다.

별로 안어렵지 않나요?

유튜브 영상을 받아오던 예시에 넣어서 다시 얘기하도록 해보겠습니다.

콜백지옥에서 헤어나오는 부분에서 이어서 얘기하는게 아닌 파카유튜브 영상 하나만 받아오는 데서부터 Promise를 적용해봅시다

let youtube;

let count = 0;

function getYoutubeChannel() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      ++count;
      resolve(`PAKA${count}번봄`);
    }, 3000);
  });
}

getYoutubeChannel().then((result) => {
  youtube = result;
  console.log(youtube);
});

콜백함수를 이용한 콜백기반 비동기처리를 Promise를 이용해서 이런식으로 변경했습니다.

3초동안 처리를하고 처리에대해서 성공이 되면 resolve('PAKA..')를 호출하였습니다.

그런데 then이라는 메소드는 처음보네요

then은 Promise객체가 가지고 있는 메소드입니다.
getYoutubeChannel이라는 함수가 리턴하는 값이 Promise이므로 then메소드를 사용할수 있는겁니다

then에 전달되는 메소드(콜백함수의 인자)의 인자로는 처리된 결과를 받습니다
첫번째 인자로는 성공에대한 결과를 받고 두번째 인자로는 실패에 대한 error를 받습니다.

getYoutubeChannel().then(
  (result) => {
    youtube = result;
    console.log(youtube);
  },
  (error) => {
    console.log(error);
  }
);

위 예시에서는 무조건 성공만을 생각하기 때문에 첫번째 인자만 처리가 되는걸 볼수가 있습니다.
실제 상황이라면 당연히 에러가 나올수 있겠죠?

처리의 성공에 대해서만 then으로 받고싶으면 인자하나만 사용하면됩니다.!

getYoutubeChannel().then((result) => {
  youtube = result;
  console.log(youtube);
});

이외에 then말고도 catch, finally를 통해 Promise에 대해서 처리할수 있습니다.

그리고 알아야할것은 프라미스 체이닝입니다.
프라미스 체이닝은 비동기 처리에 대한 결과인 resultthen핸들러의 체인을 통해서 전달된다는 점에서 착안한 아이디어입니다.

promise.then을 호출하면 또 promise가 리턴됩니다.
이를 이용해서 프라미스 체이닝을 이용할수 있습니다.

then핸들러가 값을 반환할 때 이 값이 promise의 result가 되고 그다음 then은 이 값을 이용합니다.

이를 이용해서 프라미스 체이닝을 구현해볼까요?

let youtube;

let count = 0;

function getYoutubeChannel() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      ++count;
      resolve(`PAKA${count}번봄`);
    }, 3000);
  });
}

getYoutubeChannel()
  .then((result) => {
    youtube = result;
    console.log(youtube);
    return youtube;
  })
  .then((result) => {
    youtube = result;
    console.log(youtube);
    return youtube;
  })
  .then((result) => {
    youtube = result;
    console.log(youtube);
    return youtube;
  })
  .then((result) => {
    youtube = result;
    console.log(youtube);
    return youtube;
  })
  .then((result) => {
    youtube = result;
    console.log(youtube);
    return youtube;
  });

프라미스를 리턴하는 getYoutubeChannel()함수를 한번 호출했고 이 하나의 프라미스에 대해서 프라미스 체이닝을 이용했습니다.

위의 설명과 같이 각 then 핸들러에서 리턴하는 값을 다음 then 핸들러가 result로 받아서 처리하고 있네요(then은 promise를 리턴하닌까 저게 가능한겁니다!)

콘솔에 결과는 어떻게 될까요?

3초 있다가 한번에 콘솔에 찍히네요

저희가 원하는 건 3초 (1번봄) -> 그이후 3초지남(2번봄) -> ...
를 원했는데..

저렇게 결과가 나오는 이유는 뭘까요

하나의 프라미스에 대해서 프라미스 체이닝을 했기 때문입니다.

처음에 호출한 getYouTubeChannel()이 리턴한 프라미스에 대해서만 체이닝을 했습니다.
그래서 3초를 기다리는 작업이 한번밖에 이루어지지 않고 최초 3초기다리고 받아온 파카 영상만 받아왔네요

3초를 순차적으로 기다리고 5번 영상을 제대로 받아오려면 then 핸들러에서 새로운 프라미스를 리턴하면됩니다.

getYoutubeChannel()
  .then((result) => {
    youtube = result;
    console.log(youtube);
    return getYoutubeChannel();
  })
  .then((result) => {
    youtube = result;
    console.log(youtube);
    return getYoutubeChannel();
  })
  .then((result) => {
    youtube = result;
    console.log(youtube);
    return getYoutubeChannel();
  })
  .then((result) => {
    youtube = result;
    console.log(youtube);
    return getYoutubeChannel();
  })
  .then((result) => {
    youtube = result;
    console.log(youtube);
  });

이런식으로 각각 then 핸들러에서 새로운 프라미스를 리턴했습니다. getYoutubeChannel()이 리턴하는 값이 프라미스죠?

결과를 볼까요?

저희가 원하는데로 동작하는군요

즉, 콜백지옥에서의 결과와 동일합니다.
그런데 코드는 어떤가요?

콜백지옥의 코드보다 훨씬더 간단해졌습니다.
만약 PAKA 영상을 3번째 볼때에서 에러가 발생했으면 오류를 찾기 더 간단하겠네요
코드가 옆으로 길어지지않으니 보기가 더 편하죠?

그리고 비동기적으로 실행되는 코드를 마치 동기적으로 실행되는 것처럼 보이게 합니다.

(코드가 간단해졌다는 말은 코드가 오른쪽으로 엄청길어져서 어떤곳에서 에러가 일어났는지도 찾기 어려운 상태에서 벗어났다는 의미입니다.)

이렇게 Promise의 체이닝을 이용해서 콜백지옥에서 벗어날수 있습니다.

즉, 콜백을 통해서 비동기 처리를 할수있지만 콜백만을 이용하면 코드의 가독성이 좋지 않기에 Promise를 통해서 비동기 처리를 하면 코드의 가독성이 향상되는 이점을 얻을수 있습니다.

결론


사실 이 부분은 저도 약한부분입니다.

콜백함수의 동작이 직관적이지 않아서 바로 이해하기 힘들고 Promise 자체도 자바스크립트에서 기존에 제공해주던걸 사용하기 때문에 동작하는 방식을 제대로 알지 못하면 와닿진 않습니다.

그래서 문서를 보고 나름 공부한 내용을 정리했습니다.

나는 맞다고 생각한내용이 틀린 부분일수도 있고 이해가 쉽겠지 라고 생각한 부분이 남들이 볼땐 더 어려울 수도있습니다.

그치만 자바스크립트에서 비동기적으로 동작하는 코드에 대해서 처리를 콜백함수를 통해 할수있고 Promise를 통해 할수있고 Promise chaining으로 콜백지옥에서 나올수 있다는 내용을 전달하고 싶었습니다.

다음에는 비동기를 처리하는 또다른 방법인 async await으로 오겠습니다!

0개의 댓글