ES6 - Callback

marafo·2020년 10월 28일
0

개요

웹에서 사용자의 행동을 미리 다 예측하는 것은 불가능하다. 언제 클릭할지
터치할지, 혹은 타이핑할지 알 수 없기 때문에 비동기적 실행을 필요로 한다.

자바스크립트 엔진은 단일스레드에서 동작하는데, 엄연히 한 번에 한 가지 일만 할 수 있는 것이다. 이것은 장점이자 단점일 수 있는데 멀티스레드 영역에서 발생하는 문제들을 고려하지 않아도 된다.

콜백은 프라미스와 제너레이터와 함께 비동기적 프로그래밍 방법론에서 언급되는데, 콜백이 가장 먼저 존재했고 나머지가 그 뒤를 이어 비동기적 활용을 위해 나타났다.

레스토랑이 만석 상태에 있어 손님이 대기를 할 때 전화번호를 레스토랑에 넘겨준다면 콜백, 레스토랑에서 손님에게 진동호출기를 주는 경우가 Promise와 비슷 하다고 볼 수 있다.

기본 문법

콜백은 다른 함수의 파라미터로 전달되거나 객체의 프로퍼티로 사용되는 등 나중에 호출할 함수를 의미한다.

console.log("Before timeout: " + new Date());
function f(){
  console.log("After timeout: " + new Date());
}

setTimeout(f, 60*100); // 콜백함수 호출
console.log("after setTimeout 1");
console.log("after setTimeout 2");

콘솔에서 실행한 결과는 아래와 같다. 6초 이후에 실행되도록 setTimeout에 f를 콜백함수로 넣어준 경우이다. 만약 f를 동기적으로 처리했다면 다음 동작들도 일시정지 된 상태로 6초를 기다린 후에 순차적으로 출력될 것이고 이것은 사용자에게 상당한 불편을 초래한다.

Before timeout: Wed Oct 28 2020 21:34:45 GMT+0900 (대한민국 표준시)
after setTimeout 1
after setTimeout 2
After timeout: Wed Oct 28 2020 21:34:51 GMT+0900 (대한민국 표준시)

콜백이 보통 함수 선언식으로 먼저 쓰이지 않고 다른 함수의 콜백으로 들어갈 땐 아래와 같이 익명함수를 써야 한다.

setTimeout(function(){
  console.log("After timeout: " + new Date());
}, 60*100);

그리고 대표적으로 콜백을 파라미터로 받는 함수는 setInterval, clearInterval 등이 있다.

const start = new Date();
let i = 0;
const intervalId = setInterval(function(){
  let now = new Date();
  if(now.getMinutes() !== start.getMinutes() || ++i > 10){
    return clearInterval(intervalId);
  }
  
  console.log(`${i}: ${now}`);
}, 5*1000);

∙ setInterval: 콜백 함수를 정해진 주기에 따라 호출하고 clearInterval을 사용할 때까지 멈추지 않고 실행된다. 위 코드는 실행 후 10초가 될 때까지 현재의 시간을 출력하게 된다.

스코프와 비동기적 실행

function countdown(){
  console.log('countdown:');
  // let i; 유효 스코프 범위 밖
  for(let i = 5; i >= 0; i--){
    setTimeout(function(){
      console.log(i === 0 ? '5초 경과' : i);
    }, (5 - i) * 1000);
  }
}
countdown();

여기서 setTimeout 자체는 동기적 실행이며 그 안에서 호출되는 콜백이 '비동기적 실행'이 된다. 요점은 콜백이 어느 스코프에서 선언되었는지에 따라 콜백은 자신을 선언한 스코프(여기선 for문)의 i에 접근할 수 있다.

콜백 헬(callback hell)

const fs = require('fs');

fs.readFile('a.txt', function (err, dataA) {
  if (err) console.error(err);
  fs.readFile('b.txt', function (err, dataB) {
    if (err) console.error(err);
    fs.readFile('c.txt', function (err, dataC) {
      if (err) console.error(err);
      setTimeout(function () {
        fs.writeFile('d.txt', dataA + dataB + dataC, function (err) {
          if (err) console.error(err);
        });
      }, 60 * 1000);
    });
  });
});

한 번에 연속된 대기를 하면 콜백 함수가 중첩에 중첩을 거듭하여 굉장히 복잡해질 수 있다. 이렇게 중괄호의 콜백 헬이 발생하면 원하는 시점에 한 번의 콜백 호출이 어려워진다. 따라서 이 점을 해결하기 위해 Promise가 도입되었다.

오류 우선 콜백(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 {
    // 스크립트 로딩이 성공적으로 끝남
  }
});

위 예시는 js파일을 읽어들여서 script태그를 만들고 src속성을 지정해주는 것이다.

callback의 첫 번째 파라미터는 에러로 설정한다. 에러가 발생하면 이용해 callback(new Error())가 호출된다. 두 번째 파라미터는 에러가 발생하지 않았을 때를 위해 실행되는 컨텍스트에 대한 것이다. 에러가 null이나 undefined일 때는 에러가 없는 것이고, 원하는 동작이 성공한 경우엔 callback(null, result1, result2...)이 호출된다..

참고
1) Learning JavaScript (한빛미디어)
2) https://ko.javascript.info/callbacks

profile
프론트 개발자 준비

0개의 댓글