콜백(Callback)

심현인·2021년 6월 27일
0
post-custom-banner

콜백 함수란?

다른 코드의 인자로써 이용되는 함수다. 즉 어떤 함수 A를 호출하면서 특정 조건일 때 함수 B를 실행해서 나에게 알려달라는 요청을 함꼐 보내는 것이다. 이 요청을 받은 함수 A의 입장에서는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 B를 직접 호출한다. 따라서 콜백함수는 다른 코드(함수 혹은 메소드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수다.

제어권

1. 호출 시점

var cnt = 0;
var timer = setInterval(function(){
	console.log(cnt);
  	if(++cnt > 4) clearInterval(timer);
},300)

var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);

cnt변수를 선언하고 0을 할당했다.
timer 변수를 선언하고 setInterval을 실행한 결과를 할당했다.
setInterval을 호출할 때에는 두 개의 매개변수를 전달했는데, 그 중 첫번째는 익명함수이고, 두 번째는 300이라는 숫자다.

setInterval함수에서 func는 함수, delay는 밀리초 단위의 숫자이며, 나머지 param1..은 func함수가 실행 될 때 매개변수로 전달하는 인자다. func에 넘겨준 함수는 매 delay마다 실행되며, 그 결과로 어떤 값도 리턴하지 않는다. setInterval를 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID값이 반환된다. 이것을 변수에 담는 이유는 반복 실행되는 중간에 종료(clearInterval)할 수 있게 하기 위해서이다.

다시 좀 더 보기쉽게 코드를 바꾸자면,

var cnt = 0;
var cbFunc = function(){
	console.log(cnt);
  	if(++cnt > 4) clearInterval(timer);
}
var timer = setInterval(cbFunc,300)

0.3초마다 setInterval의 첫번째 매개변수인 cbFunc(이 것이 콜백함수!) 가 실행되게 되는데 0.3초에 한 번씩 숫자가 0부터 1씩 증가하며 출력되다 4이면 종료된다. setInterval이라는 하는 '다른 코드'에 첫 번째 인자로서 cbFunc 함수를 념겨주자 제어권을 넘겨받은 setInterval이 스스로의 판단에 따라 적절한 시점(0.3초마다) 이 익명 함수를 샐행했다. 이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 갖는다.

2. 인자

var 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]
*/
var newArr = [10,20,30].map(
  function(index, currentValue){
            console.log(index, currentValue) 
  			return currentValue + 5
  			});
console.log(newArr)
/*
10 0
20 1
30 2
[15, 25, 35]
*/

map메소드는 메소드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내서 콜백 함수를 반복하고, 그 실행 결과들을 모아 새로운 배열을 만든다. 해당 매개변수들은 map 메소드의 규칙대로 동작한다. 이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값으들을 어떤 순서로 넘길 것에 대한 제어권을 갖는다.

3. this

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

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

콜백 함수는 함수다

콜백 함수로 어떤 객체의 메소드를 전달하더라 그 메소드는 메소드가 아닌 함수로 호출된다.

var obj = {
  vals: [1, 2, 3],
  logValues: function (v, i) {
    console.log(this, v, i);
  },
};

obj.logValues(1, 2);
[4, 5, 6].forEach(obj.logValues);
/*
Window {...} 4 0
Window {...} 5 1
Window {...} 6 2
*/

obj객체의 logValues메소드를 forEach의 콜백 함수로 전달했다. 메소드에서의 this는 obj를 가르키는게 맞으나, forEach에서의 콜백함수로 전달된 메소드는 함수만 전달을 했기 때문에 this가 전역객체를 바라보게 된다. 따라서 어떤 함수의 인자에 객체의 메소드를 전달하더라도 이는 결국 메소드가 아닌 함수라는 뜻!

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

그럼에도 불구하고 콜백 함수 내부에서 this가 객체를 바라보게 하고 싶다면 어떻게 해야할까?
별도의 인자로 this를 받는 함수의 경우에는 여기에 원하는 값을 넘겨주면 되지만 그렇지 않은 경우에는 this의 제어권도 넘겨주게 되므로 사용자가 임의로 값을 바꿀 수 없다. 그래서 전통적으로는 this를 다른 변수에 담아 콜백함수로 활용한 함수에서 그 변수를 사용하게 되고, 이를 클로져(?!)로 만드는 방식이 많이 쓰였다.

var obj1 = {
  name: "obj1",
  func: function () {
    var self = this;
    return function () {
      console.log(self.name);
    };
  },
};

var callback = obj1.func();
setTimeout(callback, 1000);
 

obj1.func 메소드 내부에서 self라는 변수에 this를 담고, 익명 함수를 선언과 동시에 리턴을 했다.
callback변수에 obj1.func()를 호출함과 동시에 앞서 선언한 내부함수가 반환된 것이 담긴다. 이 것을 setTimeout의 콜백함수로 전달하면 window가 아닌 obj1이 출력된다. 이 것을 좀 더 간단히 하면..

var obj1 = {
  name: "obj1",
  func: function () {
      console.log(obj1.name);
  },
};

setTimeout(obj1.func, 1000);
 

이렇게 this를 사용하지 않아도 된다. 그러나 작성한 함수를 this를 이용해 다양한 상황에 재활용을 할 수가 없게 되어 버렸다..

var obj1 = {
  name: "obj1",
  func: function () {
    var self = this;
    return function () {
      console.log(self.name);
    };
  },
};

var callback = obj1.func();
setTimeout(callback, 1000);

var obj2 = {
  name: "obj2",
  func: obj1.func,
};

var callback2 = obj2.func();
setTimeout(callback2, 1500);

var obj3 = { name: "obj3" };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
/*
obj1
obj2
obj3
*/

callback2은 obj2의 func을 실행한 결과를 담아 이 것을 콜백으로 사용했다.
callback3의 경우 obj1의 func를 실행하면서 this를 obj3이 되도록 지정해 이를 콜백으로 사용했다.

var obj1 = {
  name: "obj1",
  func: function () {
      console.log(this.name);
  },
};

setTimeout(obj1.func.bind(obj1), 1000);

var obj2 = {name:'obj2'}
setTimeout(obj1.func.bind(obj2), 1500)
/*
obj1
obj2
*/
 

bind메소드를 사용하면 위와 같이 쓸 수 있다.

콜백 지옥과 비동기 제어

콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여스기 수준이 감당하기 힘들정도로 깊어지는 현상으로, JS에서 흔희 발생하는 문제중 하나다. 주로 이벤트 처리나 서버통신과 같이 비동기적인 작업을 수행하기 위해서 이런 현태가 자주 등장하는데, 가독성이 떨어질 뿐만 아니라, 코드를 수정하기도 어렵다.

비동기는 동기의 반대말로 현재 실행중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어간다.
별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드다.

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

콜백지옥의 간단한 예이다. 0.5초마다 커피 목록을 수집하고 출력한다. 각 콜백은 커피 이름을 전달하고 목록에 이름을 추가한다. 기능 수행에는 크게 문제가 없지만 들여쓰기 수준이 가독성이 떨어지고, 전달되는 순서가 아래에서 위로 향하고 있어서 어색하다. 이러한 문제를 가장 간단히 해결할 수 있는 방법은 익명의 콜백 함수를 모두 기명함수로 바꿔주는 것이다.

var coffeeList = '';

var addEspresso = function (name) {
  coffeeList = name;
  console.log(coffeeList); // "에스프레소"
  setTimeout(addAmericano, 500, '아메리카노');
};

var addAmericano = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList); // "에스프레소, 아메리카노"
  setTimeout(addMocha, 500, '카페모카');
};

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

var addLatte = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList); // "에스프레소, 아메리카노, 카페모카, 카페라떼"
};

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

이 방식은 코드의 가독성을 향상시키고, 함수의 선언, 호출을 구분만 할 줄 안다면 위에서 아래로 순서대로 읽어내려가는데 어려움이 없습니다. 또한 변수를 최상단으로 끌어 올려서 외부에 누출됐지만 전체를 즉시 실행 함수등으로 감싸면 간단히 해결 할 수 있다.
그러나 이 방식도 코드명을 일일이 따라다녀야 하므로 헷갈릴 수도 있다.
이런 문제를 해결하기 위해 ES6에선 Promise, Generator등이 도입이 됐고,
ES2017에선 Async/Await이 도입이 됐다.

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

Promis를 이용해 콜백지옥을 해결한 예
new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만, 그 내부에 resolve 혹은 reject함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지 다음(then) 또는 오류구문(catch)으로 넘어가지 않는다. 이런식으로 비동기 작업의 동기적인 표현이 가능하다.

var addCoffee = function (name) {
  return function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        var newName = prevName ? (prevName + ', ' + name) : name;
        console.log(newName);
        resolve(newName);
      }, 500);
    });
  }
};

addCoffee('에스프레소')()
  .then(addCoffee('아메리카노'))
  .then(addCoffee('카페모카'))
  .then(addCoffee('카페라떼'))

반복되는 내용을 함수화해서 더 짧게 표현한 것.

var addCoffee = function (prevName, name) {
  setTimeout(function () {
    coffeeMaker.next(prevName ? prevName + ', ' + name : name);
  }, 500);
};

var coffeeGenerator = function* () {
  var espresso = yield addCoffee('', '에스프레소');
  console.log(espresso);
  
  var americano = yield addCoffee(espresso, '아메리카노');
  console.log(americano);
  
  var mocha = yield addCoffee(americano, '카페모카');
  console.log(mocha);
  
  var latte = yield addCoffee(mocha, '카페라떼');
  console.log(latte);
};

var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

ES6의 Generator를 이용한 것.
function뒤에 *이 붙은 함수가 Generator함수다. 이 함수를 실행하면 Iterator가 반환되는데, Iterator는 next라는 메소드를 갖고 있다. 이 next메소드를 호출하면 Generator함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다. 아후 다시 next메소드 함수를 호출하면 앞서 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield에서 실행을 멈춘다. 이런식으로 코드가 위에서 아래로 순차적으로 진행 된다.

var addCoffee = function (name) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(name);
    }, 500);
  });
};

var coffeeMaker = async function () {
  var coffeeList = '';
  var _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 vs async/await

해당 글은 개발자 단톡방에서 .then을 못쓰게 한다는 글을 보고 찾아 본 것이다.
이유는 콜백지옥때문에..
주로 영어로 된 블로그들을 참고 했는데 그 곳에서 syntax sugar라는 표현이 나오는데

  • 사람이 이해 하고 표현하기 쉽게 디자인된 프로그래밍 언어 문법
  • 사람이 프로그래밍 언어를 sweeter하게 사용 할 수 있도록 도와주는 문법
  • 더욱 더 간결하고 명확하게 표현이 가능한 문법 이라고 한다.

Promise chaining is dead. Long live async/await
해당 블로그를 잘 읽어보자..
요약은 나중에..

profile
가로
post-custom-banner

0개의 댓글