콜백

setTimeout은 스케줄링에 사용되는 가장 대표적인 함수

스크립트나 모듈을 로딩하는 것 또한 비동기 동작

 

콜백 기반(callback-based)’ 비동기 프로그래밍

<예시>

  • 스크립트 읽어오는 함수
function loadScript(src) {
  // <script> 태그를 만들고 페이지에 태그를 추가합니다.
  // 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행합니다.
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}
  • 스크립트 로딩이 끝나자마자 이 스크립트를 사용해 무언가를 해야만 한다고 가정
// script.js엔 "function newFunction() {…}"이 있습니다.
loadScript('/my/script.js'); 

newFunction(); // 함수가 존재하지 않는다는 에러가 발생합니다!

     - 에러는 브라우저가 스크립트를 읽어올 수 있는 시간을 충분히 확보하지 못했기 때문에 발생

  • loadScript 의 두 번째 인수로 스크립트 로딩이 끝난 후 실행될 함수인 콜백함수를 추가
function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}
  • 새롭게 불러온 스크립트에 있는 함수를 콜백 함수 안에서 호출
loadScript('/my/script.js', function() {
  // 콜백 함수는 스크립트 로드가 끝나면 실행됩니다.
  newFunction(); // 이제 함수 호출이 제대로 동작합니다.
  ...
});

→ 원하는 대로 외부 스크립트 안의 함수를 사용가능

 

무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 반드시 제공해야 합니다. 이렇게 콜백을 사용한 방식은 비동기 프로그래밍의 일반적인 접근법

 

콜백 속 콜백

 

스크립트가 두 개 있는 경우, 어떻게 하면 두 스크립트를 순차적으로 불러올 수 있을까요?→ 방법은 콜백 함수안에 loadScript 를 호출하는 것, 이렇게 중첩 콜백을 만들면 바깥에 위치한 loadScript가 완료된 후, 안쪽 loadScript가 실행

 

!!! 콜백 안에 콜백을 넣는 것은 수행하려는 동작이 많은 경우엔 좋지 않은 방식

loadScript('/my/script.js', function(script) {

  alert(`${script.src}을 로딩했습니다. 이젠, 다음 스크립트를 로딩합시다.`);

  loadScript('/my/script2.js', function(script) {
    alert(`두 번째 스크립트를 성공적으로 로딩했습니다.`);
  });

});

 

에러 핸들링

loadScript에서 로딩 에러를 추적할 수 있게 기능을 개선

이러한 방식으로 에러를 다루는 패턴을 ⇒ 오류 우선 콜백(error-first callback) 이라고 한다

<예시>

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생했습니다.`));

  document.head.append(script);
}

 

<사용방식>

loadScript('/my/script.js', function(error, script) {
  if (error) {
    // 에러 처리
  } else {
    // 스크립트 로딩이 성공적으로 끝남
  }
});

 

오류 우선 콜백 의 관례

  1. callback의 첫 번째 인수는 에러를 위해 남겨둡니다. 에러가 발생하면 이 인수를 이용해 callback(err)이 호출됩니다.
  2. 두 번째 인수(필요하면 인수를 더 추가할 수 있음)는 에러가 발생하지 않았을 때를 위해 남겨둡니다. 원하는 동작이 성공한 경우엔 callback(null, result1, result2...)이 호출됩니다.

⇒ 오류 우선 콜백 스타일을 사용하면, 단일 콜백 함수에서 에러 케이스와 성공 케이스 모두를 처리할 수 있습니다.

 

멸망의 피라미드

  • 꼬리에 꼬리를 무는 비동기 동작이 많아지면 깊은 중첩이 있는 코드가 만들어진다
  • 이렇게 깊은 중첩 코드가 만들어내는 패턴은 소위 ‘콜백 지옥(callback hell)’ 혹은 '멸망의 피라미드(pyramid of doom)'라고 불립니다
loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // 모든 스크립트가 로딩된 후, 실행 흐름이 이어집니다. (*)
          }
        });

      }
    })
  }
});

 

각 동작을 독립적인 함수로 만들어 위와 같은 문제를 완화하는 방법?

loadScript('1.js', step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('3.js', step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // 모든 스크립트가 로딩되면 다른 동작을 수행합니다. (*)
  }
};
  • 각 동작을 분리해 최상위 레벨의 함수로 만들었기 때문에 깊은 중첩이 없습니다.
  • 코드가 여기저기 흩어져 보이는 문제가 있다.
  • 게다가 step*이라고 명명한 함수들은 '멸망의 피라미드’를 피하려는 용도 만으로 만들었기 때문에 재사용이 불가능

→ 이를 해결할 수 있는 가장 좋은 방법 중 하나는 “프라미스” 를 사용하는 것이다.

 

참고자료

https://ko.javascript.info/async

+ Recent posts