Be lazy, Be crazy

4. 콜백함수 본문

책/코어 자바스크립트

4. 콜백함수

LAZY SIA 2022. 1. 30. 19:59
728x90
반응형

 

1-1 콜백함수란?

콜백함수

다른 함수나 메서드에게 인자를 넘겨줌으로써 그 제어권도 함께 위임한 함수

콜백함수를 위임받은 함수나 메서드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행함

 


1-2 제어권

호출 시점

예시 : setInterval(func, delay[, param1, param2, ...])

let count = 0;
const cbFunc = function() {
	console.log(count);
    if (++count > 4) clearInterval(timer);
}

const timer = setInterval(cbFunc, 300);

- timer 변수에는 setInterval의 ID값이 담긴다.

- setInterval의 첫 번째 인자인 cbFunc 함수(콜백함수)는 두 번째 인자 300ms(0.3초)마다 자동으로 실행된다.

- 콜백함수 내부에선 count를 출력하고, count 1만큼 증가시킨 다음 4보다 크면 실행을 종료하라고 한다.

=> 결과: 콘솔창에 0.3초에 한 번씩 숫자가 0부터 1씩 증가하며 출력되다가 4가 출력된 이후 종료됨

 

* 코드 실행 방식과 제어권

code 호출 주체 제어권
cbFunc(); 사용자 사용자
setInterval(cbFunc, 300); setInterval setInterval

- setInterval이라고 하는 '다른 코드'에 첫 번째 인자로서 cbFunc함수를 넘겨주자 제어권을 넘겨받은 setInterval이 스스로 판단에 따라 적절한 시점에(0.3초마다) 이 익명 함수를 실행했음.

=> 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가짐

 

 

인자

예시: map(callback[, thisArg]), -> callback: function(currentValue, index, array)

(* 제이쿼리라면 map의 콜백함수 인자의 순서는 function(index, currenValue) 로 바뀐다.)

=> 콜백 함수의 제어권을 넘겨받은 코드(여기서는 map)는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가짐

=> 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있어서 순서를 따르지 않고 코드를 작성하면 엉뚱한 결과를 얻게 됨

const newArr = [10, 20, 30].map(function (currentValue, index) {
    console.log(currentValue, index);
    return currentValue + 5;
});
console.log(newArr);

// 10 0
// 20 1
// 30 2
// [15, 25, 35]

- 배열 [10, 20, 30]의 각 요소를 처음부터 하나씩 꺼내어 콜백 함수를 실행한다

- 각 값을 출력한 다음 currentValue에 5를 더한다

- [15, 25, 35]라는 새로운 배열이 만들어져서 변수 newArr에 담긴다.

 

 

this

콜백 함수의 this

- 콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조하지만,
   제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됨

 

별도의 this를 지정하는 방식

- 동작 원리 이해를 위해 임의로 map 메서드를 작성해봄

Array.prototype.map = function (callback, thisArg) {
    const mappedArr = [];
    for (let i = 0; i < this.length; i++) {
        const mappedValue = callback.call(thisArg || window, this[i], i, this);
        mappedArr[i] = mappedValue;
    }
    return mappedArr;
};

- this에는 thisArg 값이 있으면 그 값을, 없으면 전역객체를 지정하고, 첫 번째 인자에는 메서드의 this가 배열을 가리킬 것이므로 배열의 i번째 요소(this[i])를, 두 번째 인자는 i 값을, 세 번째 인자에는 배열 자체를 지정해 호출한다.

- 그 결과 변수 mappedValue에 담겨 mappedArr의 i번째 인자에 할당

=> this에 다른 값이 담기는 이유: 제어권을 넘겨받을 코드에서 call/apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩하기 때문

 

콜백 함수 내부에서의 this

setTimeout(function () { console.log(this); }, 300);
// (1) Window { ... }

[1, 2, 3, 4, 5].forEach(function (x) {
    console.log(this)
}
// (2) Window { ... }

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector("#a").addEventListener('click', function (e) {
    console.log(this, e);
});
// (3) <button id="a">클릭</button>
// MouseEvent { isTrusted: true, ... }

- (1) setTimeout은 내부에서 콜백 함수 호출할 때 call 메서드의 첫 번째 인자에 전역객체를 넘기기 때문에 콜백 함수 내부에서의 this가 전역 객체를 가리킴

- (2) forEach는 '별도의 인자로 this를 받는 경우'에 해당하지만 별도 인자로 this를 넘겨주지 않았기 때문에 전역객체를 가리킴

- (3) addEventListener는 내부에서 콜백 함수 호출할 때 call 메서드의 첫 번째 인자에 addEventListener 메서드의 this를 그대로 넘기도록 정의돼 있기 때문에 콜백 함수 내부에서의 this는 addEventListener 호출 주체인 HTML 엘리먼트를 가리킴

 

 


1-3 콜백 함수는 함수다

- 콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출됨

 

메서드를 콜백 함수로 전달

const obj = {
    vals: [1, 2, 3]
    logValues: function(v, i) {
        console.log(this, v, i);
    }
};
obj.logValue(1, 2);			// (1) { vals: [1, 2, 3], logValues: f } 1 2
[4, 5, 6].forEach(obj.logValues);	// (2) Window { ... } 4 0
                                        // Window { ... } 5 1
                                        // Window { ... } 6 2

- (1) this는 obj를 가리키고 인자로 넘어온 1, 2가 출력

- (2) obj.logValues가 가리키는 함수만 전달.

- (2) 이 함수는 메서드로서 호출할 때가 아닌 한 obj와의 직접적인 연관이 없어짐 -> forEach에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하는 인자를 지정하지 않았으므로 함수 내부에서의 this는 전역 객체를 바라보게 됨

=> 어떤 함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 메서드가 아닌 함수일 뿐

=> 객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 바라볼 수 없다

 

 


1-4 콜백 함수 내부의 this에 다른 값 바인딩하기

- 객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 바라볼 수 없지만,

   그럼에도 콜백 함수 내부에서 this가 객체를 바라보게 하고 싶다면??

- 별도의 인자로 this를 받는 함수의 경우엔 여기에 원하는 값을 넘겨주면 되지만,
   그렇지 않은 경우에는 this의 제어권도 넘겨주게 되므로 사용자가 임의로 값을 바꿀 수 없음

 

방법1) 전통적 방식
this를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 this 대신 그 변수를 사용하게 하고, 이를 클로저로 만드는 방식

const obj1 = {
    name: 'obj1',
    func: function() {
        const self = this;
        return function() {
            console.log(self.name);
        };
    }
};
const callback = obj1.func();
setTimeout(callback, 1000)

- 1. obj1.func 메서드 내부에서 self 변수에 this를 담고 익명 함수 선언과 동시에 반환

- 2. obj1.func 호출 -> 앞서 선언한 내부 함수가 반환되어 callback 변수에 담김

- 3. callback을 setTimeout의 인자로 전달하면 1초 뒤 callback 실행되면서 'obj1'을 출력

=> 실제로 this를 사용하지도 않을뿐더라 번거로움

 

방법2) 콜백 함수 내부에서 this를 사용하지 않은 경우

const obj1 = {
    name: 'obj1',
    func: function() {
        console.log(obj1.name);
    }
};
setTimeout(obj1.func, 1000);

=> 직관적이지만, 작성한 함수를 this를 이용해 다양한 상황에서 재활용할 수 없게 되어버림

=> 처음부터 바라볼 객체를 명시적으로 지정했기 때문에 어떤 방법으로도 다른 객체를 바라보게 할 수 없음

=> 다양한 객체에 재활용할 필요 없다면 이렇게 해도 아무런 문제가 없음

 

전통적 방식에서, func 함수 재활용

// ...
const obj2 = {
    name: 'obj2',
    func: obj1.func
};
const callback2 = obj2.func();
setTimeout(callback2, 1500);

const obj3 = { name: 'obj3' };
const callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);

- callback2: obj1의 func을 복사한 obj2의 func를 실행한 결과를 담아 콜백으로 사용

- callback3: obj1의 func을 실행하면서 this를 obj3가 되도록 지정하여 이를 콜백으로 사용

-> 실행하면, 1.5초 후 'obj2', 2초 후 'obj3' 출력

=> 전통적 방식은 this를 우회적으로나마 활용함으로써 다양한 상황에서 원하는 객체를 바라보는 콜백 함수를 만들 수 있음

 

전통적 방식의 아쉬움을 보완한 방법 - ES5 bind 메서드 활용

const obj1 = {
    name: 'obj1',
    func: function() {
        console.log(this.name);
    }
};
setTimeout(obj1.func.bind(obj1), 1000);

const obj2 = { name: 'obj2' };
setTimeout(obj1.func.bind(obj2), 1500);

=> 사용자가 임의로 콜백 함수의 this 바꾸고 싶을 경우 bind 메서드를 활용하자

 


1-5 콜백 지옥과 비동기 제어

콜백 지옥

콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드 들여쓰기 수준이 깊어지는 현상

비동기 제어를 위해 콜백 함수를 사용하다 보면 콜백 지옥에 빠지기 쉬움

-> ECMAscript에는 Promise, Generator, async/await 등 콜백 지옥에서 벗어날 수 있는 훌륭한 방법 등장!

 

동기와 비동기

동기: 현재 실행 중 코드가 완료된 후에 다음 코드 실행 (즉시 처리 가능한 대부분의 코드, 시간 많이 걸려도 동기적 코드)

비동기: 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어감 (setTimeout, addEventListener, XMKHttpRequest 등. 별도의 요청, 실행 대기, 보류 등과 관련된 코드)

 

콜백 지옥 예시

setTimeout(function (name) {
    const coffeeList = name;
    console.log(coffeeList);
    
    setTimeout(function (name) {
        coffeeList += ', ' + name;
        console.log(coffeeList);
        
        setTimeout(function (name) {
            coffeeList += ', ' + name;
            conosle.log(coffeeList);
            
            setTimeout(function (name) {
                coffeeList += ', ' + name;
                console.log(coffeeList);
            }, 500, '카페라떼');
        }, 500, '카페모카');
    }, 500, '아메리카노');
}, 500, '에스프레소');

 

콜백 지옥 해결 - 기명함수로 변환

const coffeeList = '';

const addEspresso = function (name) {
    coffeeList = name;
    console.log(coffeeList);
    setTimeout(addAmericano, 500, '아메리카노');
};

const addAmericano = function (name) {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout(addMocha, 500, '카페모카');
};

const addMocha = function (name) {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout(addLatte, 500, '카페라떼');
};

const addLatte = function (name) {
    coffeeList += ', ' + name;
    console.log(coffeeList);
};

setTimeout(addEspress, 500, '에스프레소');

-> 일회성 함수를 전부 변수에 할당? 코드명 일일이 따라다녀야 해서 헷갈릴 수 있음

 

비동기 작업의 동기적 표현 - Promise

const addCoffee = function (name) {
    return function (prevName) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                const newName = precName ? (prevName + ', ' + name) : name;
                console.log(newName);
                resolve(newName);
            }, 500);
        });
    };
};
addCoffee('에스프레소')()
    .then(addCoffee('아메리카노'));
    .then(addCoffee('카페모카'));
    .then(addCoffee('카페라떼'));

- ES6 Promise를 이용

- new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then 또는 catch로 넘어가지 않음

=> 비동기 작업이 완료될 때 비로소 resolve/reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능

 

비동기 작업의 동기적 표현 - Generator

const addCoffee = function (prevName, name) {
    setTimeout(function () {
        coffeeMaker.next(prevName ? prevName + ', ' + name : name);
    }, 500);
};
const coffeeGenerator = function* () {
    const espresso = yield addCoffee('', '에스프레소');
    console.log(espresso);
    const americano = yield addCoffee(espresso, '아메리카노');
    console.log(americano);
    const mocha = yield addCoffee(americano, '카페모카');
    console.log(mocha);
    const latte = yield addCoffee(mocha, '카페라떼');
    console.log(latte);
};
const coffeeMaker = coffeeGenerator();
coffeeMaker.next();

- ES6 Generator를 이용 (*이 붙은 함수가 Generator함수)

- Generator 함수를 실행하면 Iterator가 반환

- Iterator로 next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수 실행을 멈춤

- 이후 다시 next 메서드 호출하면 앞서 멈췄던 부분에서 시작해서 그 다음 등장하는 yield에서 함수 실행을 멈춤 ...

=> 비동기 작업이 완료되는 시점마다 next 호출해주면 Generator 함수 내부 소스가 위에서부터 아래로 순차적으로 진행

 

비동기 작업의 동기적 표현 - Promise + Async/await

const addCoffee = function (name) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(name);
        }, 500);
    });
};
const coffeeMaker= async function() {
    const coffeeList = '';
    const _addCoffee = async function (name) {
        coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
    };
    await _addCoffee('에스프레소');
    console.log(coffeeList);
    await _addCoffee('아메리카노');
    console.log(coffeeList);
    await _addCoffee('카페모카');
    console.log(coffeeList);
    await _addCoffee('카페라떼');
    console.log(coffeeList);
};
coffeeMaker();

- ES2017 async/await

- 비동기 작업을 수행하고자 하는 함수 앞에 async 표기하고 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve 된 이후에야 다음으로 진행(Promise의 then과 흡사한 효과)

 

 

 

멋쟁이사자처럼 블체 코스를 수강하느라 공부가 미뤄졌다

앞으로 다시 시간내서 꾸준히 공부해서 올려야지!!

반응형
Comments