Javascript 콜백, 비동기에 대해

이지훈·2021년 2월 7일
0

공부한것들

목록 보기
4/15
post-thumbnail

시작전

Javascript를 사용하다 보면 항상 비동기라는것에 발목이 잡혔다. 당연히 코드는 위에서 아래로 순서대로 실행될거라는 학습이 되어있었기 때문일까 어느순간 될거라 믿고 실행한 함수가 정의되지 않았다거나 값이 undefined라는 에러메시지를 볼때마다 당황스러웠다. 그러다 검색을 통해 어렴풋이 이렇게 되겠거니 알고는 콜백을 넣거나 .then으로 프로미스를 사용해왔지만 항상 의문이 남았다. 어렵다고 손놓고있을순 없는것이라 이번기회에 다시한번 공부해봤다.

Javascript의 비동기?

자바스크립트의 비동기의 원리까지 설명하기 위한 포스팅은 아니라고 생각하여 간단하게 넘어가고자 한다.

이 블로그를 참고했습니다
다음 코드를 보면 first, second, third 순서대로 호출이된다.

const first = () => {
    second();
    console.log('first 호출');
}

const second = () => {
    third();
    console.log('second 호출');
}

const third = () => {
    console.log('third 호출');
}

first();
/*
third 호출
second 호출
first 호출 
*/

호출 스택에 first, second, third가 쌓이고 반대로 third, second, first가 출력될것이다.

하지만 중간에 setTimeout을 넣으면 어떨까?

const first = () => {
    second();
    console.log('first 호출');
}

const second = () => {
    setTimeout(third,1000);
    console.log('second 호출');
}

const third = () => {
    console.log('third 호출');
}

first();
/*
second 호출
first 호출
third 호출
*/

이렇게 된다. second에서 setTimeout을 통해 1초 후에 third를 실행하도록 했다. 순서대로 실행되었다면 third가 실행될때까지 기다렸다가 third, second, first순서대로 위 결과와 동일하게 나왔을 것이다. 하지만 setTimeout이 끝날때까지 기다리지 않고 second와 first가 먼저 출력됐다. 동기적이지 않고 비동기적으로 이뤄진것이다.

이벤트 루프와 비동기가 이루어지는 원리는 이해하기 조금 어렵지만 JSconf 유튜브 채널에 좋은 설명이 있어 링크를 남긴다.

호출 스택에서 비동기 호출이 생기면, 이는 백그라운드에서 실행되고 비동기 작업이 완료되면 callback queue라고 표시되어있는, 작업 큐 목록에 들어간다. 이벤트 루프는 호출 스택이 비어있다면 작업 큐 목록에서 콜백을 호출 스택으로 넣어준다.
setTimeout의 경우 1000ms로 명시하면 1초 뒤에 작업 큐 목록에 들어가게 되는것이다. 하지만 이게 1초뒤에 실행된다는 뜻은 아니다. 작업큐에 들어가는것이 1초 뒤인것이지 그때 호출 스택이 비어있지 않으면 바로 실행되지 않고 더 기다리게 될 수도 있다.
실제로 setTimeout에 0ms를 입력해도 곧바로 실행되지 않고 호출스택이 빈 후에 실행된다. 위의 예시 코드에서 0ms로 해도 결과는 second, first, third 순서가 될 것이다.

콜백

이렇게 비동기로 함수가 실행되는것을 알아보았다. 그렇다면 우리가 원하는대로 비동기 실행을 할 수 있는 방법은 뭘까? 바로 콜백이다.
콜백은 나중에 실행할 함수를 의미한다. javascript.info의 간단한 예제를 확인해보자.

콜백이 없을 때

loadScript는 매개변수로 받은 src의 스크립트를 가져오는 함수이다.

function loadScript(src) {
 // <script> 태그를 만들고 페이지에 태그를 추가합니다.
 // 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행합니다.
 let script = document.createElement('script');
 script.src = src;
 document.head.append(script);
}

그리고 이 loadScript로 가져올 /my/script.js에는 내가 사용하고싶은 함수 newFunction이 있다고 생각해보자.

// 해당 경로에 위치한 스크립트를 불러오고 실행함
loadScript('/my/script.js'); // script.js엔 "function newFunction() {…}"이 있습니다.
newFunction(); // 함수가 존재하지 않는다는 에러가 발생합니다!

난 script.js를 불러온 후, 그 안에 있는 newFunction을 사용하려고 했는데 함수가 존재하지 않는다고 에러가 발생했다. 그 이유는 loadScript가 끝날때가지 기다리지 않고 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('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 {
            // 모든 스크립트가 로딩된 후, 실행 흐름이 이어집니다. (*)
          }
        });

      }
    })
  }
});

콜백 이후

콜백으로만 해결할 수 없거나 해결하기 힘든 경우가 많다. 이를 개선하기 위해 Promise와 async-await 등이 만들어졌다. 이러한 부분은 다음에 알아보겠다.

싱글스레드라고 알고있는 js 환경에서 어떻게 비동기 백그라운드 처리를 하는지에 대해서는 아는바가 적다. 하지만 잘 정리된 블로그가 있어 이를 링크한다. 자바스크립트의 동작에 대한 이해가 부족한 것 같아 나중에 이 부분에 대해서도 따로 공부를 해봐야할것같다.

많은 도움이 되었습니다.

profile
안녕하세요! 대학교 졸업한 이지훈입니다.

0개의 댓글