[JS] 콜백 함수(Callback Function)란? [콜백 함수를 쓰는 함수 직접 만들어보기]


안녕하세요. 오늘은 JS에서 자주 사용되는 개념은 콜백 함수(Callback Function) 에 대해 알아보겠습니다.

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

const result = words.filter(word => word.length > 6);

console.log(result);
// expected output: Array ["exuberant", "destruction", "present"]

위 코드는 developer.mozilla.org 란 곳에서 가져온 배열의 filter 메소드(함수) 를 사용하는 방법을 다루고 있습니다.

JS에서 배열을 생성하면 그 배열은 .filter()라는 메소드를 가지는데 이걸 사용하면 그 배열을 제공된 규칙에 따라 제거하고, 어떤 것은 남길 수 있습니다.

 

words.filter(word => word.length > 6);

보시면 words라는 배열에서 filter을 호출하는데, filter의 첫번재 인자값에 그 배열을 걸러낼 규칙을 제공하게 됩니다. 그런데 그 규칙을 보시면 word => word.length > 6 라는 함수를 인자값으로 넘겨서 처리를 하고 있습니다.

 

사실 word => word.length > 6 이라는 표현은 화살표 함수 표현에 의해

function (word) { return word.length > 6 } 와 같은 표현입니다. 

 

<선행글 추천>
ES6에서 추가된 화살표 함수 표현을 모르시면 이 글을 읽어주세요.
(함수 선언식과 함수 표현식에 대한 내용도 포함)

 

도대체 저 함수를 제공하면 어떻게 내부적으로 동작하길레 저렇게 배열을 반환해주는 걸까요? 오늘은 저 filter()을 가지고 콜백함수에 대해 알아보겠습니다.

 

콜백 함수

먼저 콜백 함수를 간단하게 정의해보자면 다른 함수에 매개변수로 넘겨진 함수를 의미합니다.

구글링을 해보면 콜백 함수에 대한 여러 표현들이 있는데 그 중 하나를 인용해보자면 함수 안에서 실행하는 또 다른 함수라고도 할 수 있습니다.

 

아마 처음에 들으면 잘 와닿지 않을것입니다. 아래 예제를 볼까요?

 

function fn(arg){
	arg();
}

val = function() { console.log("callback!"); }
fn(val)

fn 이라는 함수의 선언부를 한번 확인해봅시다.

보시면 arg라는 매개변수를 받아서, arg(); 를 함수로 호출하고 있습니다.

 

아하! fn은 arg라는 함수를 받아서 호출하는 함수구나! 이해할 수 있습니다.

 

JS에서는 함수를 특별한 종류의 값으로 취급하기 때문에 이렇게 인자값으로 넘기거나 어떤 변수에 대입해서 호출 하는게 가능합니다. 굉장히 특이하죠. JS와 이름은 비슷하지만 전혀 다른 언어인 Java의 경우 이런 것이 허용되지 않습니다.

그러면 이제 함수를 호출하는 방법을 보겠습니다. 맨 마지막 줄에 보시면 fn(val); 이라는 형태로 fn 함수에 val 이라는 함수를 인자(입력값)로 제공해서 호출하고 있죠.

 

이러면 val이라는 함수는 fn 이라는 함수 안에서 arg로 전달되어, arg라는 이름을 갖게 되고 arg(); 를 통해 호출되게 됩니다. 즉 val은 지금 바로 실행되지 않지만 다른 함수의 입력 값으로 전달되어 다른 함수에 의해서 나중에 호출되게 되는 것 입니다!

 

이러한 것을 바로 콜백함수라고 부릅니다. 아까 정의해서 확인했듯이 다른 함수(fn)에 매개변수로 넘겨진 함수(val) 네요.

물론 val 자체는 콜백 함수는 아니지만, 이 val이라고 하는 함수가 다른 함수의 입력값으로 전달되어 그것이 나중에 호출이 된다면(Called Back) 이 맥락에서 val 은 콜백 함수(CallBack Function) 가 되는 것입니다.

 

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

const result = words.filter(word => word.length > 6);

console.log(result);
// expected output: Array ["exuberant", "destruction", "present"]

이제 아까전 개요에서 잠깐 봤던 CallBack 함수의 사례 Array Filter 로 돌아와서 콜백 함수를 조금더 이해해보겠습니다.

본 코드를 실행하면 글자가 6개보다 많은 원소만을 담은 새로운 배열을 얻을 수 있습니다.

 

const result = words.filter(word => word.length > 6);

제일 핵심인 부분은 바로 이 부분인데, "word => word.length > 6" 라는 함수를 filter라는 함수의 입력값으로 제공해주고 있습니다. 이때 word => word.length > 6 이것이 바로 콜백 함수가 되게 됩니다.

 

arr.filter(callback(element[, index[, array]])[, thisArg])

filter() 라는 함수의 원형은 다음과 같이 생겼습니다. filter() 라는 함수는 첫 번째 인자값으로 함수를 받게 되어 있습니다. 또 그 함수는 3개의 매개변수를 가지는 함수인데, 첫번째 element는 필수이고 나머지 2가지는 Optional(선택) 즉 [, index[, array]] 이 부분 index와 array는 필요하면 쓰고 필요 없으면 쓰지 않아도 되는 것 입니다.

 

간단하게 이야기해서 filter 첫번째에는 콜백 함수가 들어가는데 그 콜백 함수는 callback(element, index, array) 와 같은 형태로 넣어줘야 한다는 의미가 되겠네요.

 

callback
각 요소를 시험할 함수. true를 반환하면 요소를 유지하고, false를 반환하면 버립니다. 다음 세 가지 매개변수를 받습니다.

이 callback 함수의 형태는 알았는데 과연 callback 함수는 어떻게 동작하게 내용을 작성해줘야 하는 걸까요? 좀더 내용을 살펴보면 각 요소를 시험했을때 true를 반환하면 해당 요소를 그대로 쓰겠다는것이고, false를 반환하면 필터에 걸러져서 버리겠다. 이런 의미가 되겠군요.

 

element
처리할 현재 요소.
index Optional
처리할 현재 요소의 인덱스.
array Optional
filter를 호출한 배열.

callback 함수 인자값의 내용은 다음과 같습니다. element는 필수, 가장 중요한 것으로 우리가 판단해야 하는 각각의 원소 값들을 나타낸다라고 알려주고 있네요.

 

그러면 이 정보들을 가지고 filter에 콜백 함수를 제공해서 배열 필터링을 완성해봅시다.

 

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

function callback(element) {
	//callback 함수는 첫 번째 매개변수로 각각의 원소를 줄 것이라고 약속되어 있음.
    console.log(element);
}

words.filter(callback);

spray
limit
elite
exuberant
destruction
present

우리는 words 라는 배열에서 .filter() 을 이용해 필터링을 할 것인데, 아까도 말했듯이 필터의 기준을 함수로 만들어서 입력해 달라는 것이 요구사항입니다.

 

callback 함수는 첫 번째 매개변수로 각각의 원소를 줄 것이라고 약속되어 있으므로 이것을 element라는 이름으로 받고 console.log 로 찍어보면 모든 배열의 원소들이 차례대로 출력됨을 볼 수 있습니다.

 

사실 filter 함수가 내부적으로 어떻게 동작하는진 모르지만 확실한 건 내부적으로 모종의 처리를 끝낸 다음에 어떤 시점에서 우리가 인자로 준 callback 함수를 나중에 호출 함으로써 동작하고 있는 것이죠.

 

이제 우리가 해야할 것은 각각의 원소가 원하는 것인지, 그렇지 않은 것인지 알려줘야 합니다. 원하면 true 를 반환하고, 원하지 않으면 false를 반환하라고 했었죠.

 

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

function callback(element) {
	return element.length > 5;
}

filtered_words = words.filter(callback);
console.log(filtered_words);

['exuberant', 'destruction', 'present']

기준은 각 요소가 5글자 이상인지 제공

즉, element.length > 5 를 이용하겠습니다.

비교 연산자(>) 에 의해 만약에 element.length > 5 가 참이라면 true값이 나올 것이고 거짓이라면 false 값이 나올 것 입니다. 그 값을 그대로 return 으로 던져주면 끝나겠죠.

 

이제 코드를 살짝 정리해보겠습니다.

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

filtered_words = words.filter(function (element) {
	return element.length > 5;
});
console.log(filtered_words);

['exuberant', 'destruction', 'present']

filter에 제공하는 콜백 함수 callback 은 사실 필터링을 위해 딱 한번만 사용되는 함수입니다. 재활용을 할필요가 없으므로 위 처럼 익명 함수로 작성해주면 좋겠죠. 또 이렇게 함수 선언과 사용하는 곳이 멀리 떨어져 있다면 코드를 읽는데도 불편함이 따릅니다.

 

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

filtered_words = words.filter(element => element.length > 5);
console.log(filtered_words);

['exuberant', 'destruction', 'present']

ES6의 화살표 함수를 이용해 한번더 간결화된 표현을 적어주면 아까 처음 예제에서 봤던 코드와 동일해집니다!

이제 여기까지 왔으면 해당 코드의 의미를 대강 이해하셨을 것이고, 표현 자체가 매우 깔끔하고 직관적이라는 것을 이해하게 됩니다. (안타깝게도 내부적인 작동 매커니즘은 직관적이지 못한거 같습니다.)

 

콜백 함수를 소비하는 함수 만들어보기

filter라는 함수는 우리가 공급한 콜백 함수를 소비합니다.

이러한 filter이 어떻게 동작하는지 알고자 이 filter 함수를 직접 만들어보는 시간을 가져보겠습니다.

 

filter라는 이름 대신 우리가 만들 필터는 custom_filter 라는 함수 이름으로 짓고, 첫 번째 요소에는 필터링을 할 원본 배열 그리고 2번째는 필터링의 기준이 되는 콜백함수를 인자로 받아봅시다.

 

function custom_filter(origin_array, callback) {
  //결과값을 저장할 배열
  let result = [];

  //for of 를 이용해서 배열 요소를 순회한다.
  for (element of origin_array) {
    if (callback(element)) {
      //들어온 callback 함수를 기준으로 element를 구분한다.
      //callback 함수의 반환 값이 참이면 그 값을 담으면 됨.
      result.push(element);
    }
  }

  return result; //결과 값을 return 한다.
}

만들 custom_filter 함수의 선언은 다음과 같습니다.

origin_array로 원본 배열을 받고 for of 를 통해서 배열 요소를 하나 하나씩 순회합니다.

그리고 순회하면서 element를 구분할 조건을 파악하기 위해 들어온 callback 함수를 호출합니다.

 

지금 넣어준 callback 함수는  element => element.length > 5 와 같은 함수인데 이 기준에 맞춰서 값이 callback 함수에서 true false로 반환되어서 나오겠죠.

 

만약에 참이라면 result 배열에 담고 거짓이라면 그냥 담지 않고 넘어가면 됩니다.

for문 순회가 끝났으면 처리된 result 배열을 반환하면 될 것 입니다.

 

function custom_filter(origin_array, callback) {
  //결과값을 저장할 배열
  let result = [];

  //for of 를 이용해서 배열 요소를 순회한다.
  for (element of origin_array) {
    if (callback(element)) {
      //들어온 callback 함수를 기준으로 element를 구분한다.
      //callback 함수의 반환 값이 참이면 그 값을 담으면 됨.
      result.push(element);
    }
  }

  return result; //결과 값을 return 한다.
}

const words = [
  "spray",
  "limit",
  "elite",
  "exuberant",
  "destruction",
  "present",
];

filtered_words = custom_filter(words, (element) => element.length > 5);
console.log(filtered_words);

그리고 함수에 원본 배열과 콜백 함수를 제공해보면 우리가 아까 사용했던 filter와 동일하게 작동을 하는걸 볼 수 있습니다! 

 

element.length > 5 대신에 console.log(element) 로 함수 내용을 변경해서 값을 찍어봐도 값들이 순회하면서 잘 나오는데 실제로 filter 내부에서 for문을 돌리면서 callback(element) 를 통해 콜백 함수를 호출해주고 있기 때문에 그런 것이죠.

 

사실 글 쓰는 지금까지도 일반적인 프로그래밍 패턴에선 볼 수 없었던 내용들이라 난해하고 생소하긴 합니다만.. JS코드를 작성할 때 (특히 Node.js 같은..) 꼭 나오는 패턴이기 때문에 한번쯤 알고 넘어가시면 좋습니다.

 

콜백함수의 용도

콜백함수는 위 filter() 함수처럼 어떤 일을 처리할 때 기준이 될 수 있도록 사용될 수도 있지만 일을 순차적으로 처리하고 싶을 때도 많이 씁니다.

 

function a(callback) { 
    //First
    // a lot of hard work...
    callback()
}

function b() {
	//Second
}

a(b)

예를 들어서 a() 라는 함수의 일을 처리한 이후에 b() 라는 함수를 바로 실행하고 싶을때는 어떻게 해야할까요?

 

배운 콜백함수를 사용해보면 a라는 함수의 인자값으로 콜백함수를 받고 a함수에서 내부의 처리를 끝낸 이후에 콜백함수를 호출하면 될 것 입니다.

 

사용할 땐 a(b); 로 호출해서 b를 콜백함수로 전달하면 a 함수의 일이 끝난 이후에 바로 b라는 일을 시킬 수 있는 것이죠.

이런 특징을 이용해서 네트워킹 통신을 할 때 언제까지고 값을 기다릴 수 없으니 콜백함수와 같이 네트워킹 통신 API 함수를 호출해주고, 값이 오면 안에 있는 콜백 함수를 나중에 호출(실행)해서 사용해라 라고 지시할 수 있는것입니다.

 

물론 콜백함수 자체는 특별한 것이 아니라 아까도 정의했듯이 그냥 어떤 함수의 인자값으로 넘겨진 함수를 의미합니다. 당장은 실행되지 않지만 어떤 함수의 인자값으로 넘겨져서 나중에 호출될 뿐인 것이죠. 나중에 호출된다고 해서 CallBack 이라고 하는 것입니다.

 

 

 

<참고>

https://www.youtube.com/watch?v=TAyLeIj1hMc&t=724s 

 

COMMENT WRITE