[자바스크립트] 콜백 함수(Callback Function)

minidoo·2020년 10월 6일
20
post-thumbnail

콜백함수(Callback Function) 란?

파라미터로 함수를 전달하는 함수

콜백함수(Callback Function)란 파라미터로 함수를 전달받아, 함수의 내부에서 실행하는 함수이다.

let number = [1, 2, 3, 4, 5];

number.forEach(x => {
    console.log(x * 2);
});

<output>
2
4
6
8
10

콜백함수는 이미 우리의 코드 속에서 자주 사용되고 있다.
예를 들어, forEach 함수의 경우 함수 안에 익명의 함수를 넣어서 forEach 문을 동작시킨다.


콜백함수(Callback Function) 사용 원칙

익명의 함수 사용

let number = [1, 2, 3, 4, 5];

number.forEach(function(x) {
    console.log(x * 2);
});

위의 예제를 화살표 함수에서 일반 함수로 바꾼 예제이다.
콜백함수는 이름이 없는 '익명의 함수'를 사용한다. 함수의 내부에서 실행되기 때문에 이름을 붙이지 않아도 된다.

함수의 이름(만) 넘기기

function whatYourName(name, callback) {
    console.log('name: ', name);
    callback();
}

function finishFunc() {
    console.log('finish function');
}

whatYourName('miniddo', finishFunc);

<output>
name: miniddo
finish function
[ TIP ]

JavaScript Data Type
- number, string, boolean, object(function, array, data, regexp), null, undefined

자바스크립트는 nullundefined 타입을 제외하고 모든 것을 객체로 다룬다.
함수를 변수 or 다른 함수의 변수처럼 사용할 수 있다. 함수를 콜백함수로 사용할 경우, 함수의 이름만 넘겨주면 된다.
위의 예제에서, 함수를 인자로 사용할 때 callback, finishFunc 처럼 () 를 붙일 필요가 없다는 것이다.

전역변수, 지역변수 콜백함수의 파라미터로 전달 가능

  • 전역변수(Global Variable) : 함수 외부에서 선언된 변수
  • 지역변수(Local Variable) : 함수 내부에서 선언된 변수
let fruit = 'apple';	// Global Variable

function callbackFunc(callback) {
    let vegetable = 'tomato';	// Local Variable
    callback(vegetable);
}

function eat(vegetable) {
    console.log(`fruit: ${fruit} / vegetable: ${vegetable}`);
}

callbackFunc(eat);

<output>
fruit: apple / vegetable: tomato

콜백함수(Callback Function) 주의할 점

this를 사용한 콜백함수

let userData = {
    signUp: '2020-10-06 15:00:00',
    id: 'minidoo',
    name: 'Not Set',
    setName: function(firstName, lastName) {
        this.name = firstName + ' ' + lastName;
    }
}

function getUserName(firstName, lastName, callback) {
    callback(firstName, lastName);
}

getUserName('PARK', 'MINIDDO', userData.setName);

console.log('1: ', userData.name);
console.log('2: ', window.name);

<output>
1: Not Set
2: PARK MINIDDO

우리는 첫 번째 콘솔의 값이 PAKR MINIDDO 이기를 기대했지만, Not Set이 출력된다.
setName() 함수가 실행되기 전의 name 값이 나오는 것인데, 이는 getUserName() 이 전역 함수이기 때문이다.

즉, setName()에서 사용된 this 객체가 window라는 글로벌 객체를 가리킨다.
따라서 this를 보호할 수 있도록 콜백함수를 만들어야 한다.

해결 방안 : call()apply()를 사용하여 this를 보호할 수 있다.

  • call() : 첫 번째 인자로 this 객체 사용, 나머지 인자들은 , 로 구분
  • apply() : 첫 번째 인자로 this 객체 사용, 나머지 인자들은 배열 형태로 전달
// call

...

function getUserName(firstName, lastName, callback, obj) {
    callback.call(obj, firstName, lastName);	- (1)
}

getUserName('PARK', 'MINIDDO', userData.setName, userData);	- (2)

console.log(userData.name);

<output>
PARK MINIDDO

( 2 ) 에서 마지막 인자에 담긴 userData 는 ( 1 )에서 call 함수의 첫번째 인자로 전달된다.
즉, call() 에 의해서 userDatathis 객체가 매핑된다.

apply() 도 인자를 배열로 전달한다는 점만 다르고 동일하게 작동한다.

// apply

...

function getUserName(firstName, lastName, callback, obj) {
    callback.apply(obj, [firstName, lastName]);
}

getUserName('PARK', 'MINIDDO', userData.setName, userData);

console.log(userData.name);

<output>
PARK MINIDDO

콜백지옥 (Callback Hell)

비동기 호출이 자주 일어나는 프로그램의 경우 '콜백 지옥'이 발생한다.
함수의 매개변수로 넘겨지는 콜백 함수가 반복되어 코드의 들여쓰기 수준이 감당하기 힘들어질 정도로 깊어지는 현상이다.

function add(x, callback) {
    let sum = x + x;
    console.log(sum);
    callback(sum);
}

add(2, function(result) {
    add(result, function(result) {
        add(result, function(result) {
            console.log('finish!!');
        })
    })
})

<output>
4
8
16
finish!!

해결 방안 : Promise를 사용하여 콜백지옥을 탈출할 수 있다.

function add(x) {
    return new Promise((resolve, reject) => {
        let sum = x + x;
        console.log(sum);
        resolve(sum);
    })
}

add(2).then(result => {
    add(result).then(result => {
        add(result).then(result => {
            console.log('finish!!');
        })
    })
})

<output>
4
8
16
finish!!

Promise 는 정상 수행 후 resolve, 실패 후 reject 가 실행된다.
callback을 사용했던 것과 마찬가지로 resolve에 값을 담아 전달한다.

하지만, 이 패턴도 그리 좋은 방법은 아니다. 결국 콜백지옥처럼 들여쓰기 수준을 감당하기 힘들어진다.

해결 방안 : Promise의 return 사용하여 Promise Hell을 탈출할 수 있다.

프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다. 다만 최종 결과를 반환하지는 않고, 대신 프로미스를 반환해서 미래의 어떤 시점에 결과를 제공합니다.

MDN 에서 정의하고 있는 Promise에 대한 설명이다.
프로미스는 비동기 호출 시, 마치 동기 호출 처럼 값을 반환할 수 있다는 문구에 집중해보자.
즉, resolve를 통해 전달 받은 값을 반환하여 사용해야 한다!

function add(x) {
    return new Promise((resolve, reject) => {
        let sum = x + x;
        console.log(sum);
        resolve(sum);
    })
}

add(2).then(result => {
    return add(result);
}).then(result => {
    return add(result);
}).then(result => {
    console.log('finish!!');
})

<output>
4
8
16
finish!!

마무리하며

자바스크립트 스터디를 시작하며, '콜백함수' 강의를 맡게 되었다.
정의만 알았을 뿐 이렇게 자세히 들여다 본 적은 없었는데 차근차근 따라가니 어느정도 이해 할 수 있었다.

아마도 콜백 함수의 결론은 "Promise와 async/await를 사용하자"가 아닐까?
forEach, map과 같이 간단하게 사용할 수 있는 함수를 제외하고 들여쓰기가 많아지는(복잡해지는) 함수는 최대한 피해야할 것 같다. 비동기와 동기에 대한 개념도 쓸 때마다 헷깔리는데 이렇게 한 번 정리해야 할 것 같다 :-)


참고 사이트

https://yubylab.tistory.com/entry/자바스크립트의-콜백함수-이해하기
https://wooooooak.github.io/javascript/2018/12/08/call,apply,bind/

1개의 댓글

comment-user-thumbnail
2022년 10월 18일

❤ 누르려고 회원가입 했습니다!!!
너무 좋은 글 감사합니다!!!
https://cookie-clicker.co/

답글 달기