[Day22] Javascript -콜백함수(Callback Function), 고차함수(HOF)

Validator·2023년 7월 11일
0

콜백함수(Callback Function)

콜백함수는 함수를 활용하는 방법중 하나이다
정확하게는 콜백함수는 파라미터(매개변수, 인자)로 전달받은 함수를 말한다.
파라미터로 콜백함수를 전달받고 함수 내부에서 필요할 때 콜백함수를 호출한다는 개념이다.
(그래서 이름도 Callback Function이라고 붙여진 것이다)

즉, 콜백 함수란 파라미터로 일반적인 변수나 값을 전달하는 것이 아닌 함수 자체를 전달하는 것을 말한다고 보면 된다. 또한 어차피 매개변수에 함수를 전달해 일회용으로 사용하기 때문에 굳이 함수의 이름을 명시할 필요가 없어 보통 콜백 함수 형태로 함수를 넘겨줄때 함수의 이름이 없는 '익명 함수' 형태로 넣어주게 된다.

예제)
add함수로 리턴된 값을 printResult함수의 인자로 전달해 주었다.

function add(x, y) {
    return x + y
}

function printResult(result) {
    console.log(result)
}

printResult(add(10,20))

위 코드를 콜백함수로 구현하면 다음과 같이 변경할 수 있다.
add함수에 콜백함수를 받을 print라는 파라미터를 추가하고 내부에서 x와 y의 합을 인자로 전달해 주었다.
그리고 print파라미터의 인자로 printResult함수를 전달해 주었다.
바로 여기서 printResult함수가 콜백함수가 되는 것이다!
먼저 add 함수가 호출된 후 printResult함수가 add함수 내부에서 나중에 호출되게 된다..

function add(x, y, print) {
    print(x + y)
}

function printResult(result) {
    console.log(result)
}

add(10, 20, printResult)

또한 콜백함수는 정의된 함수 뿐만아니라 익명 함수도 인자로 전달 할 수 있다.
보통 이렇게 파라미터에다가 콜백함수를 넣을 때는 Arrow Function을 주로 이용한다.
(간단하게 표현 가능한 점 때문)

function add(x, y, print) {
    print(x + y)
}

add(10, 20, (result) => {
    console.log(result)
})

콜백함수의 장단점

콜백함수를 사용하면서 얻는 이점은 다음과 같다.

함수를 인자로 받기 때문에 필요에 따라 함수의 정의를 달리해 전달할 수 있습니다.
함수를 굳이 정의하지 않고 익명 함수로도 전달 가능합니다.
비동기(Asynchronous) 처리 방식의 문제점을 해결할 수 있습니다.

하지만 콜백함수를 사용하면 다음과 같은 단점이 존재한다.

콜백함수를 너무 남용하면 코드의 가독성이 떨어집니다. (콜백 지옥)
에러 처리가 어렵다.

콜백함수를 이용한 비동기처리

콜백함수는 주로 비동기처리에 사용된다. 비동기(Asynchronous)란 특정 코드의 실행이 끝날 때 까지 기다리지 않고 다음 코드로 바로 넘어가는 것을 의미한다.

대표적으로 자바스크립트에 내장되있는 setTimeout() 이라는 함수가 존재한다.

callback 이라는 파라미터를 선언하고 콜백함수를 전달 받아 setTimeout() 함수의 인자로 전달하였다.

setTimeout() 함수는 비동기 함수이기 때문에 코드의 실행이 끝날 때 까지 기다리지 않고 바로 다음코드로 넘어가게 된다.

그렇기 때문에 Hello가 먼저 출력되고 1(1000ms)초 뒤에 콜백함수가 실행 되는 것이다.

function callBackTestFunc(callback) {
    setTimeout(callback, 1000)
    console.log('Hello')
}

callBackTestFunc(() => {
    console.log('waited 1 second')
})
Hello
waited 1 second

보통 파라미터(인자)로 콜백함수를 요구하는 함수나 메소드들이 있다. 그렇기 때문에 콜백함수의 개념을 잘 이해하고 있어야 한다.


콜백 함수 사용 원칙

  1. 익명의 함수 사용

위에서 소개했듯이 보통 콜백 함수는 호출 함수에 일회용으로 사용하는 경우가 많아, 코드의 간결성을 위해 이름이 없는 '익명의 함수'를 사용한다. 함수의 내부에서 매개변수를 통해 실행되기 때문에 이름을 붙이지 않아도 되기 때문이다.

JAVASCRIPT
sayHello("Hello", function (name) { // 함수의 이름이 없는 익명 함수
	console.log(name); 
});

이밖에도 뜻하지 않은 개발자의 실수로 인한 함수 이름의 충돌 방지를 위한 이유도 있다. 콜백함수에 이름을 붙이면, 그 이름은 함수 스코프 내에서 유효한 식별자가 되는데, 만약 같은 스코프 내에 이미 같은 이름의 식별자가 있다면, 콜백함수의 이름이 기존의 식별자를 덮어쓰게 되어 버린다. 이는 의도치 않은 결과를 초래할 수 있다. 예를 들어, 아래 코드에서는 변수 add와 콜백함수의 이름이 add로 설정할 경우, 콜백 함수가 변수의 값을 변경해 버리게 된다.

let add = 10; // 변수 add

function sum(x, y, callback) {
  callback(x + y); // 콜백함수 호출
}

// 이름 있는 콜백함수 작성
sum(1, 2, function add(result) {
  console.log(result); // 3
});

// 변수 add가 함수 add가 되어버린다. --> 이런 사태를 방지하기 위해 익명함수를 쓰는 것!
console.log(add); // function add(result) {...}
  1. 화살표 함수 모양의 콜백

콜백 함수를 익명 함수로 정의함으로써 코드의 간결성을 얻을 수 있었지만, 한단계 더 간결성을 얻기 위해 자바스크립트의 화살표 함수를 통해 '익명 화살표 함수' 형태로 정의해 사용할 수 있다. 이러한 익명 화살표 콜백 함수 형태는 앞으로 자바스크립트 프로그래밍을 하면서 정말 자주 접하게 되는 형태이다.

JAVASCRIPT
function sayHello(callback) {
var name = "Alice";
callback(name); // 콜백 함수 호출
}

// 익명 화살표 콜백 함수
sayHello((name) => {
console.log("Hello, " + name);
}); // Hello, Alice

  1. 함수의 이름을 넘기기

자바스크립트는 일급 객체Visit Website의 특성을 가지고 있기 때문에, 자바스크립트는 null과 undefined 타입을 제외하고 모든 것을 객체로 다룬다. 그래서 매개변수에 일반적인 변수나 상수값 뿐만 아니라 함수 자체를 객체로서 전달이 가능한 것이다. 만일 콜백 함수가 일회용이 아닌 여러 호출 함수에 재활용으로 자주 이용될 경우, 별도로 함수를 정의하고 함수의 이름만 호출 함수의 인자에 전달하는 식으로 사용이 가능하다.

// 콜백 함수를 별도의 함수로 정의
function greet(name) {
  console.log("Hello, " + name);
}

function sayHello(callback) {
  var name = "Alice";
  callback(name); // 콜백 함수 호출
}

function sayHello2(callback) {
  var name = "Inpa";
  callback(name); // 콜백 함수 호출
}

// 콜백 함수의 이름만 인자로 전달
sayHello(greet); // Hello, Alice
sayHello2(greet); // Hello, Inpa

이러한 특징을 응용하면, 매개변수에 전달할 콜백 함수 종류만을 바꿔줌으로서 여러가지 함수 형태를 다양하게 전달이 가능하다. 아래와 같이 다른 동작을 수행하는 함수 say_hello 와 say_bye 를 정의해두고 introduce 함수의 입력값으로 각기 다른 콜백 함수를 전달해주면, introduce라는 함수에서 다른 동작을 수행하는 것이 가능해진다.

function introduce (lastName, firstName, callback) {
    var fullName = lastName + firstName;
    
    callback(fullName);
}

function say_hello (name) {
    console.log("안녕하세요 제 이름은 " + name + "입니다");
}

function say_bye (name) {
    console.log("지금까지 " + name + "이었습니다. 안녕히계세요");
}

introduce("홍", "길동", say_hello);
// 결과 -> 안녕하세요 제 이름은 홍길동입니다

introduce("홍", "길동", say_bye);
// 결과 -> 지금까지 홍길동이었습니다. 안녕히계세요


콜백 함수 활용 사례

위 처럼 직접 콜백 함수를 만들어 사용하기도 하지만, 이미 우리는 자바스크립트에서 콜백 함수를 사용하는 여러가지 메서드를 이용해왔었다.

이벤트 리스너로 사용

addEventListener는 특정 이벤트가 발생했을 때 콜백 함수를 실행하는 메서드이다. 클릭과 같은 이벤트를 처리하기 위해 등록하는 이벤트 리스너로 콜백함수가 쓰인다. 버튼을 클릭하면 그에 연관되는 스크립트 실행을 콜백 함수로 등록하는 형태인 것이다.

let button = document.getElementById("button"); // 버튼 요소를 선택

// 버튼에 클릭 이벤트 리스너를 추가
button.addEventListener("click", function () { // 콜백 함수
  console.log("Button clicked!"); 
});

고차함수에 사용

자바스크립트에서 for문 보다 더 자주 사용되는 반복문이 forEach 메서드일 것이다. 이 역시 forEach 메서드의 입력값으로 콜백 함수를 전달하는 형태임을 볼 수 있다.

// 예시 : 배열의 각 요소를 두 배로 곱해서 새로운 배열을 생성하는 콜백 함수 
let numbers = [1, 2, 3, 4, 5]; // 배열 선언 
let doubled = []; // 빈 배열 선언 

// numbers 배열의 각 요소에 대해 콜백 함수 실행 
numbers.forEach(function (num) { 
    doubled.push(num * 2); // 콜백 함수로 각 요소를 두 배로 곱해서 doubled 배열에 추가 
}); 

console.log(doubled); // [2, 4, 6, 8, 10]

타이머 실행 함수로 사용

setTimeout이나 setInterval과 같은 타이머 함수에서 일정 시간마다 스크립트를 실행하는 용도로서 콜백 함수를 이용한다.

// 3000 밀리초(3초) 후에 콜백 함수 실행
setTimeout(function () {
  console.log("Time is up!"); // 콜백 함수로 콘솔에 메시지 출력
}, 3000);


고차함수 (Higher-Order Function)

고차 함수(Higher order function)는 함수를 인자로 전달받거나 함수를 결과로 반환하는 함수를 말한다. 다시 말해, 고차 함수는 인자로 받은 함수를 필요한 시점에 호출하거나 클로저를 생성하여 반환한다. 자바스크립트의 함수는 일급 객체이므로 값처럼 인자로 전달할 수 있으며 반환할 수도 있다.

// 함수를 인자로 전달받고 함수를 반환하는 고차 함수
function makeCounter(predicate) {
  // 자유 변수. num의 상태는 유지되어야 한다.
  let num = 0;
  // 클로저. num의 상태를 유지한다.
  return function () {
    // predicate는 자유 변수 num의 상태를 변화시킨다.
    num = predicate(num);
    return num;
  };
}

// 보조 함수
function increase(n) {
  return ++n;
}

// 보조 함수
function decrease(n) {
  return --n;
}

// makeCounter는 함수를 인수로 전달받는다. 그리고 클로저를 반환한다.
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// makeCounter는 함수를 인수로 전달받는다. 그리고 클로저를 반환한다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

고차 함수는 외부 상태 변경이나 가변(mutable) 데이터를 피하고 불변성(Immutability)을 지향하는 함수형 프로그래밍에 기반을 두고 있다. 함수형 프로그래밍은 순수 함수(Pure function)와 보조 함수의 조합을 통해 로직 내에 존재하는 조건문과 반복문을 제거하여 복잡성을 해결하고 변수의 사용을 억제하여 상태 변경을 피하려는 프로그래밍 패러다임이다. 조건문이나 반복문은 로직의 흐름을 이해하기 어렵게 하여 가독성을 해치고, 변수의 값은 누군가에 의해 언제든지 변경될 수 있어 오류 발생의 근본적 원인이 될 수 있기 때문이다.

함수형 프로그래밍은 결국 순수 함수를 통해 부수 효과(Side effect)를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이려는 노력의 한 방법이라고 할 수 있다.

자바스크립트는 고차 함수를 다수 지원하고 있다. 특히 Array 객체는 매우 유용한 고차 함수를 제공한다. 이들 함수에 대해 살펴보도록 하자.

.forEach()

for문을 대체하는 고차 함수.
반복문을 추상화하여 구현된 메서드이고 내부에서 주어진 배열을 순회하면서 연산을 수행

map()

forEach 같이 순회하면서, 콜백함수에서의 실행결과를 리턴한 값으로 이루어진 배열을 만들어 반환

[ forEach와 map의 차이 ]

​두 메서드 모두 배열을 순회하는 것은 동일하지만,
forEach()의 경우 각 요소를 참조한 연산이 이루어지고
map()의 경우엔 각 요소를 다른 값으로 맵핑한 새로운 배열이 반환되는 점에 차이가 있다.

정리하면 forEach()는 for문을 대체하여 사용하고 map()은 연산의 결과로 새로운 배열을 생성하고자할 때 사용된다.

.find()

indexOf() 가 찾고자 하는 값을 인덱스로 주는거고,
include()가 찾고자 하는 값을 Bool로 주는거면,
find()는 찾고자 하는 값을 그대로 반환한다

주어진 배열을 순회하면서 콜백 함수 실행의 반환값이 true에 해당하는 첫번째 요소를 반환
(첫번째 요소만 반환하기 때문에, 이 사실을 반드시 인지하고 있어야 한다!!)

.findIndex()

배열 메소드 indexOf() 의 콜백함수 버젼.
고차함수 find()의 리턴값이 인덱스인 버젼.

.filter()

주어진 배열을 순회하면서 콜백 함수의 반환값이 true에 해당하는 요소로만 구성된 새로운 배열을 생성하여 반환.
한마디로 find()의 찾아서 값을 반환하는 기능과 map()의 배열 생성 기능의 융합 버젼.

.sort()

배열 정렬.
단, 복사본이 만들어지는게 아니라 원 배열이 정렬됨.
콜백 함수를 통해 배열의 원소들을 어느 기준으로 정렬할지 지정해야함
(ex. return a-b (ascending 오름차순) / return b-a (descending 내림차순))


위와 같이 문자를 정렬할때는 문제가 없지만, 숫자를 정렬하는 경우에도 ABC 순으로 정렬이 되기 때문에 콜백함수를 넣어 조작이 필요하다.

const arr = [2, 1, 3, 10];

arr.sort(function(a, b)  {
  return a - b;
});  // [1, 2, 3, 10] 오름차순

arr.sort(function(a, b)  {
  return b - a;
}); // [10, 3, 2, 1] 내림차순

📌 객체 정렬

const arr = [
  {name: 'banana', price: 3000}, 
  {name: 'apple', price: 1000},
  {name: 'orange', price: 500}
];

arr.sort(function(a, b) {
  return a.price - b.price; // price 숫자값을 기준으로 정렬
});
/*
{"name":"orange","price":500}
{"name":"apple","price":1000}
{"name":"banana","price":3000}
*/

.reduce()

콜백 함수의 실행된 반환값(initialValue)을 전달 받아 연산의 결과값이 반환.
첫번째 인자(accumulator)서부터 시작해서 배열값인 두번째 인자(currentvalue) 을 순회하며 accumulator+=currentvalue 을 실행.
사실상 forEach, map, filter기능을 reduce로 모두 구현해서 쓸순 있어 고차함수의 부모라고 불림

**Tip**

reduce()함수 호출시 initialValue 값이 없는 경우
- accumulator : 배열의 첫번째 값
- currentValue : 배열의 두번째 값

reduce()함수 호출시 initialValue 값이 있는 경우
- accumulator : initialValue가 지정한 값
- currentValue : 배열의 첫번째 값
const numberArr = [1, 2, 3, 4, 5];

let test = numberArr.reduce((accumulator, currentvalue, index, array) => {
  console.log(`accumulator : ${accumulator}`);
  console.log(`currentvalue : ${currentvalue}`);
  console.log(`index : ${index}`);
  console.log(`array : ${array}`);
  console.log(`return값 : ${accumulator + currentvalue}`);
  console.log("---------------------------------");
  return accumulator + currentvalue;
}, 0);

console.log(test);

출력 결과는 다음과 같다!

0개의 댓글