[JS] 비동기 처리의 시작 콜백, 그리고 콜백 지옥

2021. 4. 11. 17:04Javascript/문법

 최근에 면접을 봤다. Promise, async/await를 이용해서 비동기 처리를 어떻게 하는지에 대한 질문에 제대로 답변을 하지 못했다. 그래서 이번에 Promise, async/await를 제대로 알고가자를 목표로 왜 필요한지, 어떻게 쓰이는지에 대해서 확실하게 정리해보고자 한다.

 

 

 이번 포스팅에서는 Promise가 왜 필요한가에 대해서 써보려고 한다. Promise가 필요한 이유비동기 함수를 동기화시키기 위해서라고 간단히 말할 수 있지만, 이 답변이 나오기까지의 과정은 그리 간단하지 않다. 그래도 나는 그 어려운 과정을 최대한 쉽게 풀어서 써보려고 한다(피드백, 비판은 언제든지 환영입니다).

 

 

1. 자바스크립트는 동기적으로 움직인다.

우리가 먼저 알아가야할 문장이다. 정말 그럴까? 다음 코드를 입력해서 브라우저 콘솔에서 출력 결과를 확인해보자.

console.log("첫 번째 순서");
console.log("두 번재 순서");
console.log("세 번째 순서");

 

어떻게 나왔는가? 당연하게도 "첫 번째 순서", "두 번째 순서", "세 번째 순서" 순서대로 출력됐을 것이다. 이는 자바스크립트 동작이 동기적으로 움직이기 때문이다. 이쯤되면 동기적, 비동기적이라는 단어가 어떻게 다른지 헷갈릴 것이다.

 

 

[그림 1] 이어달리기와 마라톤의 차이

 

 두 단어의 결정적인 차이는 순서가 있느냐 없느냐다. 추가적으로 설명을 덧붙이자면, 이어 달리기는 먼저 뛴사람에게 바통을 받아야 다음 사람이 뛸 수 있는 시스템을 가지고 있지만, 마라톤은 바통이 있든말든 먼저 달려서 1등만 하면 된다.

 

 

 따라서 다음 코드의 출력결과는 위와 엄연히 다르다.

console.log("첫 번째 순서");
setTimeout(() => console.log("두 번째 순서"), 1000);
console.log("세 번째 순서");

 

 

2. 콜백 함수

아래 코드에서 사용했던 setTimeout은 자바스크립트에서 많이쓰이는 비동기 함수다. setTimeoutconsole.log("첫 번째 순서")의 동작이 끝나든 말든 자신의 차례가 오면 바로 동작한다. 또한, 자신의 동작이 끝날 때까지 기다리지 말라고 다음 코드를 바로 실행할 수 있게 양보해주는 좋은 녀석이다.

console.log("첫 번째 순서");
setTimeout(() => console.log("두 번째 순서"), 1000);
console.log("세 번째 순서");

  

보통 setTimeout 같은 비동기 함수콜백함수를 파라미터로 요구한다. 여기서 콜백함수란, 나중에 호출할 함수를 의미한다. 콜백함수는 경우에 따라 동기적 콜백함수비동기적 콜백함수로 나뉠 수 있다. 밑에 두 코드를 실행시켜 출력결과를 확인해봤으면 한다.

const printWithImmediately = (callback) => {
  callback();
}

/* 동기적인 콜백함수 */
printWithImmediately(() => console.log("첫 번째 순서"));
printWithImmediately(() => console.log("두 번째 순서"));
printWithImmediately(() => console.log("세 번째 순서"));
const printInLater = (callback, delay) => {
  setTimeout(callback, delay);
}

/* 비 동기적인 콜백함수 */
const callback = () => {
  console.log(`함수 시작 시간: ${new Date().toString()}`)
}

printInLater(callback, 1000);
printInLater(callback, 1000);
printInLater(callback, 1000);

 

3. 비동기 처리의 필요성

위 코드를 예시로

const printInLater = (callback, delay) => {
  setTimeout(callback, delay);
}

/* 비 동기적인 콜백함수 */
const callback = () => {
  console.log(`함수 시작 시간: ${new Date().toString()}`)
}

printInLater(callback, 1000);
printInLater(callback, 1000);
printInLater(callback, 1000);

 

세 개의 출력결과 모두 같다는 것을 볼 수 있다. 이유는 세 개 모두 1초 후에 실행되었기 때문이다(사실 비동기 처리해야하는 경우는 엄청나게 다양하다). 그런데 1초 후에 세 개를 동시에 실행시키지 않고, 1초마다 함수를 실행시키고 싶다면 콜백을 체이닝 시키면된다. 

const printInLater = (callback, delay) => {
  setTimeout(() => {
    id = "첫 번째"
    callback(id);

    setTimeout(() => {
      id = "두 번째"
      callback(id);

      setTimeout(() => {
        id = "세 번째"
        callback(id);
      }, delay);

    }, delay);
  }, delay);
}

/* 콜백 체이닝을 이용해 1초 간격으로 세 개의 콜백함수가 출력된다. */
const callback = (id) => {
  console.log(`[order: ${id}]함수 시작 시간: ${new Date().toString()}`)
}

printInLater(callback, 1000);

 

위와 같이 코드를 작성하면, 각각의  함수는 전에 실행됐던 함수가 끝나야 실행된다. 그런데, 이 방식으로 함수를 열 번 사용해야한다면? 이 글을 읽는 당신은 엄청나게 가독성이 떨어지는 콜백지옥을 경험하게 된다.

/* 콜백지옥의 시작 */ 

const printInLater = (callback, delay) => {
  setTimeout(() => {
    id = "첫 번째"
    callback(id);
    setTimeout(() => {
      id = "두 번째"
      callback(id);
        setTimeout(() => {
          id = "세 번째"
          callback(id);
          setTimeout(() => {
            id = "네 번째"
            callback(id);
            setTimeout(() => {
                id = "다섯 번째"
                callback(id);
              }, delay);
            }, delay);
          }, delay);
      }, delay);
    }, delay);
}

 

4. Promise

위 상황에 근거하여 Promise가 필요한 이유를 두 가지로 나타낼 수 있다.

 

  1.  비동기 함수를 동기화시키기 위해 사용
  2.  콜백지옥을 해결하기 위해 사용

위 두 문제를 해결하기 위해 Promise를 다음과 같이 사용할 수 있다.

const printInLater = (delay) => {
  return new Promise((resolve) => {
      setTimeout(() => {
        resolve(new Date().toString());
      }, delay)
  })
}

const callback = (id, result) => {
  console.log(id, result);
}

/* Promise를 이용해서 다음과 같이 비동기 처리, 콜백지옥 문제를 해결한다.*/
printInLater(1000).then((result) => {
  callback("첫 번째", result);
  return printInLater(1000);
}).then((result) => {
  callback("두 번째", result);
  return printInLater(1000);
}).then((result) => {
  callback("세 번째", result);
  return printInLater(1000);
}).then((result) => {
  callback("네 번째", result);
  return printInLater(1000);
}).then((result) => {
  callback("다섯 번째", result);
});

 

 

5. 맺음말

여기까지 왜  Promise가 필요한지에 대해서 정리해보았다. 이번 포스팅에서는 오직 setTimeout을 이용해서 예시 코드를 짰다. 하지만 비동기 처리를 해야하는 상황은 엄청나게 다양하다고 한다. 특히 Axios, Fetch를 이용해서 API 데이터를 가져와야하는 경우에 많이 사용될 것 같다. Promise 사용법에 관해서는 다음 포스팅에서 정리해보고자 한다. 긴 글 읽어주셔서 감사합니다.

 

 

6. 참고자료