자바스크립트 콜백 (callbacks)

콜백함수를 다른 함수의 인자로 전달하여, 특정 이벤트가 발생하거나 특정 작업이 완료되면 실행되도록 하는 메커니즘입니다.

이를 통해 비동기적인 작업이나 이벤트 처리 등에 유용하게 활용됩니다.

콜백 이란?

callback 함수의 간단한 예제는 아래와 같습니다.

// define function to be used as a callback function
function A() {
   console.log('function A');
}

// define function that triggers callback function
function B(callback) {
   callback();
}

// execute function that triggers callback function
B(A); // expected log: 'function A'

위 예제에서 함수 A, 함수 B, 2개의 함수를 정의했습니다. 그리고 함수 B에 함수 A를 콜백함수로 전달했습니다.

function Z() {
   console.log('function Z');
}

function X() {
   console.log('function X');
}

function Y() {
   console.log('function Y');
}

B(Z); // expected log: 'function Z'
B(X); // expected log: 'function X'
B(Y); // expected log: 'function Y'

이 예를 보면 함수 Z, X, Y를 정의하고 이를 함수 B 내부의 콜백 함수로 전달하여 매번 다른 논리가 실행된다는 것을 확인 할 수 있습니다.

익명 콜백 함수

익명 함수를 콜백으로 전달하는 것도 가능합니다. 함수 W를 전달해보고 그 다음으로 W를 제거하여 익명함수로 만들어 콜백함수를 만들어보겠습니다.

// W함수가 콜백 함수로 전달 됨
B(
  function W() {
     console.log('function W');
  }
); // expected log: 'function W'


// 익명 함수가 콜백 함수로 전달 됨
B(
  function () {
     console.log('anonymous function');
  }
); // expected log: 'anonymous function'

ES6 Syntax을 이용한 콜백 함수

ES6에서는 화살표 함수 표현식을 생성할 수 있습니다. 화살표 함수 표현식을 사용하면 function 키워드를 사용할 필요 없이 함수를 정의하는 동시에 인수와 본문 대괄호 사이에 => 표현식을 사용할 수도 있습니다.

콜백으로 사용될 때 어떻게 보이는지 확인해 보겠습니다.

// calbacks defined using arrow function expressions
B(
  () => {
     console.log('anonymous function');
  }
); // expected log: 'anonymous function'

B(
  () => console.log('one liner anonymous function')
); // expected log: 'one liner anonymous function'

const anotherOneLinerFn = () => console.log('another one liner anonymous function');
B(anotherOneLinerFn); // expected log: 'another one liner anonymous function'

콜백 실행 순서

이전의 모든 예제는 함수 B가 수행하는 유일한 작업이 콜백을 실행하는 것이므로 함수 B를 정의해야 하는 필요성이 있는지 궁금해하게 만들 수 있습니다.

콜백 대신 함수를 직접 호출하면 어떨까요?

모든 콜백을 실행하는 함수에 추가 논리를 추가할 수 있다는 점이 중요합니다. 따라서 다른 로직을 실행하기 위해 함수 B를 수정하기로 결정한 경우 이를 수행하고 콜백 함수를 계속 실행할 수 있습니다.

// modifying logic of the function that triggers callback function
function B(callback) {
   // function's B internal logic
   const date = new Date();
   console.log('This is the year ' + date.getFullYear());

   // execute callback function at the end
   callback();
}

이러한 방식으로 어떤 콜백 함수가 전달되는 함수 B는 항상 This is the year ${currentYear}가 찍히게 만들 수 있습니다.

B(A);
// expected result
// expected log: 'This is the year 2021'
// expected log: 'function A'

B(Y);
// expected result
// expected log: 'This is the year 2021'
// expected log: 'function Y'

B(X);
// expected result
// expected log: 'This is the year 2021'
// expected log: 'function X'

처음에 언급했듯이 콜백 함수는 함수의 끝에서 실행되는 것이 일반적이지만 항상 함수의 끝에서 실행될 필요는 없습니다. 다른 함수의 상단, 중간 또는 끝에서 콜백 함수를 트리거할 수도 있습니다.

// modifying logic of the function that triggers callback function
function B(callback) {
   // execute callback function right away prior executing additional logic
   callback();

   // function's B internal logic
   const date = new Date();
   console.log('This is the year ' + date.getFullYear());

}
B(A);
// expected result
// expected log: 'function A'
// expected log: 'This is the year 2021'

B(Y);
// expected result
// expected log: 'function Y'
// expected log: 'This is the year 2021'

B(X);
// expected result
// expected log: 'function X'
// expected log: 'This is the year 2021'

콜백의 타입

동기 콜백, 비동기 콜백 2 종류가 있습니다.

동기 콜백 (Synchronous Callbacks)

동기 콜백은 다른 함수 내부에 정의된 작업 순서에 따라 다른 함수에서 순차적으로 트리거됩니다. 콜백 실행이 완료되지 않으면 다른 함수가 모든 내부 논리를 완료하기 위해 콜백을 호출하는 것을 방지합니다. 다음 다이어그램을 살펴보겠습니다.

위에 보이는 getCurrentMonth 함수는 네 가지 다른 프로세스를 가진다고 해봅니다.

Get Current Date
Log Current Date
Execute Callback
Return Month

코드로는 아래와 같습니다.

function getCurrentMonth(callback) {
   // Get Current Date
   const date = new Date();

   // Log Current Date
   console.log('This is the current Date' + date.toString());

   // execute callback function
   callback();

   return date.getMonth() + 1;
}

동기 콜백 주의점

대부분의 경우 동기 콜백이 사용됩니다. 그러나 콜백 함수의 성능이 좋지 않으면 콜백 함수를 실행하는 함수의 성능에도 영향을 미칩니다. 다음 코드를 살펴보겠습니다.

function goodPerformanceFn() {
  console.log('good performance');
}

function badPerformanceFn() {
  for (var p = 0; p < 1000; p++) {
    console.log('bad performance');
  }
}

function getCurrentMonth(callback) {
   const t0 = performance.now();

   // Get Current Date
   const date = new Date();

   // Log Current Date
   console.log('This is the current Date' + date.toString());

   // execute callback function
   callback();

   // log how long it took to trigger all processes prior returning month
   const t1 = performance.now();
   console.log('Took: ' + (t1 - t0) + 'msecs');

   return date.getMonth() + 1;
}

getCurrentMonth(goodPerformanceFn);
getCurrentMonth(badPerformanceFn);

터미널이나 브라우저 개발자 도구의 콘솔에서 이 코드 조각을 실행하면 getCurrentMonth 함수에 의해 트리거된 각 프로세스가 완료되는 데 걸리는 시간을 확인할 수 있습니다.

goodPerformanceFn 를 콜백함수로 실행하면 완료까지 0.3 msec가 걸립니다.
반면에 getCurrentMonth 를 콜백함수로 실행하면 완료까지 95.4 msec이 걸립니다.
이는 goodPerformanceFn를 수행하는 것보다 300배 이상 오래 걸리는 것입니다.

이와 같은 프로그램이 작업이 끝날 때까지 멈춰있는 것처럼 동작하게 됩니다.

비동기 콜백 (Asynchronous Callbacks)

비동기 콜백은 콜백을 호출하는 함수 내에서 코드 실행이 차단되는 것을 방지하는 데 사용됩니다.

이전 예제에서 getCurrentMonth에서 badPerformanceFn를 호출하면 시간이 굉장히 오래 걸리는 것을 확인하였습니다.

이번에는 badPerformanceFn 함수를 리팩토링하여 reFactoredBadPerformanceFn 함수를 아래와 같이 만들어 봅니다.

function reFactoredBadPerformanceFn() {
  setTimeout(function () {
    for (var p = 0; p < 1000; p++) {
      console.log('refactored bad performance');
    }
  }, 0)
}

getCurrentMonth(reFactoredBadPerformanceFn)

이제는 getCurrentMonth 에 reFactoredBadPerformanceFn 을 콜백해도 month 데이터를 기다림 없이 출력하는 것을 확인 할 수 있습니다.

setTimeout에 대한 경험이 많지 않은 경우 timeout 매개변수가 0으로 설정되어 있으므로 혼란스러울 수 있습니다. 이는 논리적으로 내부 함수 핸들러를 실행하기 위해 0밀리초를 기다리는 것을 의미합니다. 이론상으로는 맞지만 함정이 있습니다.

setTimeout(fn, 0)을 실행한다고 해서 함수가 즉시 실행된다는 보장은 없습니다. 무슨 일이 일어나고 있는지 이해하려면 이벤트 루프가 무엇인지 이해해야 합니다.

이벤트 루프는 코드 실행을 담당하는 JavaScript에서 사용하는 메커니즘이자 JavaScript 비동기 프로그래밍의 이유입니다. 다음 다이어그램을 확인해 보겠습니다.

Call Stack (호출 스택):
– 프로그램이 함수를 호출할 때 해당 함수의 정보를 담은 스택입니다.
– 호출한 함수는 스택의 맨 위에 쌓이고, 함수가 종료되면 스택에서 제거됩니다.
– 위의 코드에서는 Get Current Date, Log Current Date, Execute Callback, Return Month 가 call stack으로 들어오게 됩니다.
– 여기서 비동기 함수인 setTimeout 를 제외한 함수들을 여기서 동작 완료하게 됩니다.

Web API (브라우저 API):
– 브라우저 환경에서 제공되는 API로, 비동기 작업을 수행합니다.
– 비동기 작업(예: setTimeout)이 발생하면 이 작업은 브라우저의 Web API로 이동하여 별도의 스레드에서 실행됩니다. 이렇게 하면 메인 코드의 실행이 차단되지 않습니다.

Callback Queue (콜백 큐):
– 비동기 작업이 완료되면 비동기 작업에 등록된 콜백(처리할 함수)이 콜백 큐에 들어갑니다.
– 여기서는 setTimeout 으로 0 밀리초가 지난 후 반복문이 들어간 함수가 콜백 큐로 들어가는 겁니다.

Event Loop (이벤트 루프):
– 콜 스택과 콜백 큐를 주시하며, 콜 스택이 비어있을 때 콜백 큐에 있는 작업을 콜 스택으로 가져와 실행시킵니다.
– 위의 코드에서는 반복문을 구성된 함수가 콜백 스택으로 이동하고 실행되는 겁니다.

비동기 콜백이 중요한 이유

콜백은 특정 이벤트가 발생했을 때 실행할 프로세스를 정의하는 데 중요합니다.

예를 들어, 클릭(onclick), 포커스 획득(onfocus), 포커스 상실(onblur)과 같은 DOM 이벤트가 발생할 때 콜백 함수를 실행하는데 사용됩니다.

동기적 처리의 문제점을 해결 할 수 있습니다.

예를 들어 두 명의 사용자가 동시에 두 개의 다른 API 엔드포인트에 요청을 보내면, 첫 번째 요청을 처리하는 데 시간이 오래 걸리면 두 번째 요청은 대기해야 합니다.
두 번째 요청자는 첫 번째 요청이 완료될 때까지 기다려야 하므로, 블로킹이 발생하게 됩니다.

참고. 콜백 함수 VS 클로저 ?

콜백 함수를 공부해보니 파이썬의 클로저와 함수를 인자로 받는다는 점이 유사한 것 같아서 둘의 차이를 정리해보았습니다.

자바스크립트 콜백 함수

function fetchData(callback) {
  // 비동기 작업 수행
  setTimeout(function() {
    const data = 'Fetched data!';
    callback(data);
  }, 2000);
}

fetchData(function(result) {
  console.log(result);
});

파이썬 클로저

def outer_function(x):
    def inner_function(y):
        return x + y
    return inner_function

closure_example = outer_function(10)
result = closure_example(5)
print(result)  # 출력: 15

클로저와 콜백 함수 둘 다 함수를 변수에 할당하거나 함수의 인자로 전달하는 등의 유연한 활용이 가능합니다.

다만, 콜백 함수는 주로 비동기 작업에서 사용되고, 클로저는 주로 함수 내부에서 변수의 상태를 유지하거나 함수 팩토리로 활용됩니다. 개념적으로는 다르지만, 각각의 용도에 따라 유사한 패턴을 보일 수 있습니다.

참고할 만한 글

Leave a Comment

목차