[JavaScript] 비동기와 콜백 - 쉽게 이해하기

Lpla

·

2021. 3. 7. 23:08

반응형

비동기와 콜백을 깊이 공부하고자 하는 분에게는 맞지 않은 포스팅입니다.

 

1. 비동기

비동기콜백, 자바스크립트를 2년 가까이 공부하면서 나에게 가장 어려운 개념이었다.

용어가 생소하고 각종 사이트를 찾아봐도 한 문장으로 설명해주는 곳이 없었기 때문이다.

 

사실 이 두 개념을 몰라도 보통 난이도 이하의 자바스크립트 코드를 짜는데 별 어려움이 없었다.

이유는 알 수 없지만 함수를 찾을 수 없다는 에러 메시지가 뜬다? 혹은 실행 순서가 꼬인다?

그렇다면 함수 위치를 바꾸거나 요소가 존재할 때 실행되도록 하거나, 편법으로 setTimeout을 사용하면 대체로 해결됐다.

 

나는 비동기와 콜백을 잘 사용하는 것이 아니라 이것들이 대체 무엇인지 알고 싶었다.

 

 

따라서 이 글은 비동기와 콜백을 어느 정도 알고 있다면 시간 낭비이므로 안 읽는 것을 추천한다.

 

 

먼저 아주 쉬운 아래 코드가 있다.

 

JavaScript

setTimeout(function() {
 console.log('초코칩 프라푸치노 나왔습니다.')
}, 60000)

console.log('아메리카노 나왔습니다.')

// 결과
// 아메리카노 나왔습니다.
// 60초 뒤
// 초코칩 프라푸치노 나왔습니다.

들어가기 전에 알아둬야 할 점이 있다.

우리는 setTimeout()이 시간을 지연시키는 자바스크립트 함수라는 것을 알고 있다.

이 글에서 setTimeout은 비동기를 설명하기 위한 예시일 뿐 실제로는 의도하지 않았지만 무언가로 인해 ??초 뒤에 실행되는 함수라고 생각해야 한다.

 

이제 위 코드를 살펴보자.

나는 초코칩 프라푸치노를 먼저 호출했는데 실제로는 아메리카노가 먼저 콘솔창에 결과를 띄웠다.

내 의도와는 달리 순차적(혹은 동시)으로 호출되지 않고 비순차적(혹은 비동시)에 호출된 것이다.

이것이 비동기이다.

 

동기 방식은 코드가 직관적이고 작성한 대로 실행되니 헷갈릴 일이 적다. 하지만 다음 작업을 가만히 기다려야 하는 단점이 있다.

비동기 방식은 코드는 복잡할 수 있지만 결과를 기다리는 동안 다른 작업을 수행할 수 있어 처리 속도가 빠르다는 장점이 있다.

 

동기 카페와 비동기 카페 중 어디에서 음료를 주문하고 싶은가?

나는 아메리카노 한 잔을 마시기 위해 앞서 주문한 음료와 샌드위치를 20분씩 기다려야 하는 동기 카페는 절대 싫다.

 

 

 

2. 콜백 함수

이제 비동기보다 어려운 콜백에 대해서 알아보자.

앞선 코드에서 초코칩 프라푸치노는 60초 뒤에 나온다.

다시 말해서 60초 뒤에 초코칩 프라푸치노가 콜백된다.

콜백 함수는 어떤 특정한 시점에 호출되는 함수를 말한다.

 

코딩을 하다 보면 종종 실행 순서를 반드시 지켜야 하는 경우가 있다.

반드시 초코칩 프라푸치노가 아메리카노보다 먼저 나와야 하는 상황 말이다.

이때 콜백 함수를 사용하여 해결할 수 있다.

 

 

function order(callback) {
  setTimeout(function(){
    console.log('초코칩 프라푸치노 나왔습니다.');
    callback();
  }, 3000);
}

order(function() {
  console.log('아메리카노 나왔습니다.');
});

// 결과
// 3초 뒤
// 초코칩 프라푸치노 나왔습니다.
// 아메리카노 나왔습니다.

핵심은 callback 이라는 매개변수다.

새로운 order 함수를 만들고 비동기 함수 마지막에 매개변수를 호출했다.

콜백 함수로 간단하게 비동기 방식의 처리 문제를 해결했다.

 

 

 

3. 콜백 지옥

function order(callback) {
  setTimeout(function(){
    console.log('초코칩 프라푸치노 나왔습니다.');
    (function() {
      setTimeout(function(){
        console.log('티라미수 나왔습니다.');
        (function() {
          setTimeout(function(){
            console.log('그린티라떼 나왔습니다.');
            (function() {
              setTimeout(function(){
                console.log('망고 블렌디드 나왔습니다.');
                callback();
              },1500);
            })();
          },1500);
        })();
      },1500);
    })();
  }, 1500);
}

order(function() {
  console.log('아메리카노 나왔습니다.');
});

// 결과
// (1.5초 뒤) 초코칩 프라푸치노 나왔습니다.
// (1.5초 뒤) 티라미수 나왔습니다.
// (1.5초 뒤) 그린티라떼 나왔습니다.
// (1.5초 뒤) 망고 블렌디드 나왔습니다.
// 아메리카노 나왔습니다.

콜백 함수를 사용하면 코드의 실행 순서가 꼬이는 것을 막을 수 있다.

하지만 콜백 함수를 중첩해서 사용하게 되면 코드는 걷잡을 수 없이 지저분해진다.

이를 해결하기 위한 방법으로 보통 세 가지가 추천된다.

 

 

3-1. 기명 함수

가장 단순한 방법은 함수를 분리시키는 것이다.

하지만 위 콜백 함수는 익명 함수로 만들었기 때문에 이대로 분리시킬 수 없다.

익명 함수를 이름이 있는 기명 함수로 바꿔야 한다.

 

let order1 = function order1(name) {
  console.log(name + ' 나왔습니다.');
  setTimeout(order2, 500, '티라미수');
};

let order2 = function order2(name) {
  console.log(name + ' 나왔습니다.');
  setTimeout(order3, 500, '그린티라떼');
};

let order3 = function order3(name) {
  console.log(name + ' 나왔습니다.');
  setTimeout(order4, 500, '망고 블렌디드');
};

let order4 = function order4(name) {
  console.log(name + ' 나왔습니다.');
  order5('아메리카노');
}

let order5 = function order5(name) {
  console.log(name + ' 나왔습니다.');
}

setTimeout(function() {
  order1('초코칩 프라푸치노')
}, 500);

이제 코드의 깊이가 줄어들었고 훨씬 깔끔해졌다.

여기에 화살표 함수를 사용하면 코드를 조금 더 줄일 수 있다.

 

let order1 = (name) => {
  console.log(name + ' 나왔습니다.');
  setTimeout(order2, 500, '티라미수');
};

let order2 = (name) => {
  console.log(name + ' 나왔습니다.');
  setTimeout(order3, 500, '그린티라떼');
};

let order3 = (name) => {
  console.log(name + ' 나왔습니다.');
  setTimeout(order4, 500, '망고 블렌디드');
};

let order4 = (name) => {
  console.log(name + ' 나왔습니다.');
  order5('아메리카노');
}

let order5 = (name) => {
  console.log(name + ' 나왔습니다.');
}

setTimeout(() => {
  order1('초코칩 프라푸치노')
}, 500);

 

 

3-2. Promise

프라미스는 ES6에 도입된 비동기 처리에 사용되는 객체이다.

new Promise()로 프라미스를 호출하고 resolve와 reject를 매개변수로 사용한다.

여기서 프라미스를 자세히 설명하진 않을 것이다.

 

new Promise(function (resolve) {
  setTimeout(function () {
    resolve();
    console.log('초코칩 프라푸치노 나왔습니다.');
  }, 500);
})
.then(function () {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
      console.log('티라미수 나왔습니다.');
    }, 500);
  });
})
.then(function () {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
      console.log('그린티라떼 나왔습니다.');
    }, 500);
  });
})
.then(function () {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
      console.log('망고 블렌디드 나왔습니다.');
    }, 500);
  });
})
.then(function () {
  return new Promise(function (resolve) {
    resolve();
    console.log('아메리카노 나왔습니다.');
  });
});

프라미스를 사용할 때는 new Promise()와 .then 메서드를 유의하면 된다.

프라미스를 아래처럼 사용할 수도 있다.

 

order1()
.then(order2)
.then(order3)
.then(order4)
.then(order5)

function order1() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
      console.log('초코칩 프라푸치노 나왔습니다.');
    }, 500);
  });
}

function order2() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
      console.log('티라미수 나왔습니다.');
    }, 500);
  });
}

function order3() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
      console.log('그린티라떼 나왔습니다.');
    }, 500);
  });
}

function order4() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
      console.log('망고 블렌디드 나왔습니다.');
    }, 500);
  });
}

function order5() {
  return new Promise(function (resolve) {
    resolve();
    console.log('아메리카노 나왔습니다.');
  });
}

 

 

3-3. async, await

async와 await는 프라미스의 단점을 해결하기 위해 ES7에 등장한 개념이다.

async는 함수 앞에 위치하는데 해당 함수는 항상 프라미스를 반환한다.

await는 async 함수 안에서만 사용할 수 있다.

async, await는 나도 아직 익숙하지 않으므로 방법만 소개하겠다.

 

async function order() {
  await order1('초코칩 프라푸치노');
  await order1('티라미수');
  await order1('그린티라떼');
  await order1('망고 블렌디드');
  await order2('아메리카노');
}

function order1(item) {
  return new Promise(resolve => setTimeout(() => {
    console.log(item + ' 나왔습니다.');
    resolve();
  }, 500));
}

function order2() {
  return new Promise(resolve => {
    console.log('아메리카노 나왔습니다.');
    resolve();
  });
}

order();

 

 

비동기와 콜백은 나도 아직 공부하는 단계라 틀린 부분이 있을 수 있다.

제대로 공부하려면 아래 사이트를 추천한다. (다만 쉽게 이해는 안 된다... 어렵다!)

나도 제대로 공부한 다음 정리해서 포스팅하도록 하겠다.

 

 

프라미스와 async, await 공부 사이트

 

프라미스와 async, await

 

ko.javascript.info

 

반응형