CoreJS - 콜백 함수

SANGKU OH·2020년 12월 1일
0

CoreJavascript

목록 보기
7/10
post-thumbnail

콜백 함수?

콜백 함수는 다른 코드의 인자로 넘겨주는 함수이다.

콜백 함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 실행할 것이다.
모르겠다구요? 더 쉽게 예를 들어보자.

A와 B는 다음 날 아침 8시에 만나기로 하고 잠을 잡니다. 약속 장소에 가려면 늦어도 6시에는 일어나야 합니다. A는 불안한 마음에 수시로 깨어 시계를 확인합니다. 계속 잠을 설치다가 결국 5시 즈음 포기하고 일어나고야 맙니다. 한편 B는 알림시계를 세팅합니다. 시계가 정한 시각에 울리지 않을 염려는 없고 평소 알람소리에 쉽게 눈을 뜨곤 했던지라 안심하고 꿀잠을 잡니다. 6시가 되자 시계의 알람소리를 듣고 상쾌하게 일어납니다.

A는 수시로 시간을 구하는 함수를 직접 호출한 것이다. 반면 B는 시계의 알람을 설정하는 함수를 호출했고, 해당 함수는 호출 당시에는 아무것도 하지 않다가 B가 정해준 시각이 됐을 때 비로소 '알람을 울리는'결과를 반환한다.

시간정보를 제공하는 시계입장에서 생각해보면 A의 경우 요청할 때마다 수동적으로 시간 정보를 제공하기만 한 반면, B의 경우에는 요청을 받은 뒤 자체적으로 무언가를 수행하다가 적절한 시점에 적극적으로 통보했다.
A의 경우 시계함수의 제어권은 A에게 있고, 시계는 그저 요청받은 내용을 이행할 뿐이다. 그런데 B는 시계 함수에게 요청을 하면서 알람을 울리는 명령에 대한 제어권을 시계에게 넘겨준 것이다. 이처럼 콜백 함수는 제어권과 관련이 깊다!

제어권

호출 시점

var count = 0;
var cbFunc = function() {
  console.log(count);
  if(++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300)
  • timer 변수에는 setInterval의 ID 값이 담긴다.
  • setInterval에 전달한 첫 번쨰 인자인 cbFunc 함수(이 함수가 곧 콜백 함수이다)는 0.3초마다 자동으로 실행될 것이다.
  • 콜백 함수 내부에서는 count 값을 출력하고, count를 1만큼 증가시킨 다음, 그 값이 4보다크면 반복 실행을 종료하라고 한다.
  • setInterval이라고 하는 '다른 코드'에 첫 번째 인자로서 cbFunc 함수를 넘겨주자 제어권을 넘겨받은 setInterval이 스스로 판단에 따라 적절한 시점에(0.3초마다)이 익명 함수를 실행했다.

이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.

인자

var newArr = [10, 20, 30].map(function(currentValue, index){
  console.log(curr, idx);
  return curr + 5;
});
console.log(newArr);
// -- 실행 결과 --
// 10 0
// 20 1
// 30 2
// [15, 25, 35]
  • 1번째 줄에서 newArr 변수를 선언하고 우항의 결과를 할당했다.
  • 5번째 줄에서 그 결과를 확인하고자 한다.
  • 1번째 줄의 우항은 배열 [10, 20, 30]에 map 메소드를 호출하고 있다.
  • 이때 첫 번째 매개변수로 익명 함수를 전달한다.

map에 대해 알아보자면

Array.prototype.map(callback[, thisArg])
callback: function(currentValue, index, array)
  • map 메소드는 첫 번째 인자로 callback 함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있다.
  • thisArg를 생략할 경우에는 일반적인 함수와 마찬가지로 전역객체가 바인딩된다.
  • map메소드는 메소드의 대상이 되는 배열의 모든 요소를 처음부터 끝가지 하나씩 꺼내여 콜백 함수를 반복 호출하고, 콜백함수의 실행 결과들을 모아 새로운 배열을 만든다.
  • 콜백 함수의 첫 번째 인자에는 배열의 요소 중 현재값이, 두 번째 인자에는 현재값의 인덱스가, 세 번째 인자에는 map 메소드의 대상이 되는 배열 자체가 담긴다.
  • 첫 번째에 대한 콜백 함수는 currentValue에 10이, index에는 인덱스 0이 담긴 채 실행된다.
  • 각 값을 출력한 다음 15(10 + 5)를 반환한다.
  • 두 번째(인덱스 1)에 대한 콜백 함수는 currentValue에 20이, indexdpsms 1이 담긴 채로 실행, 25(20 + 5)를 반환한다.
  • 같은 방식으로 세 번째에 대한 콜백 함수까지 실행을 마치고 나면, 이제는 [15, 25, 35]라는 새로운 배열이 만들어져서 변수 newArr에 담기고, 이 값이 5번째 줄에서 출력된다.

this

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

별도의 this를 지정하는 방식 및 제어권에 대한 이해를 높이기 위해 map 메소드를 직접 구현해보자!
Array.prototype.map 구현

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

메소드 구현의 핵심은 call/apply 메소드에 있다!

this에는 thisArg 값이 있을 경우에는 그 값을, 없을 경우에는 전역객체를 지정하고, 첫 번째 인자에는 메소드의 this가 배열을 가리킬 것이므로 배열의 i번째 요소 값을, 두 번째 인자에는 i 값을, 세 번째 인자에는 배열 자체를 지정해 호출한다. 그 결과 변수 maapedValue에 담겨 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를 넘겨주지 않았기 때문에 전역객체를 가리키게 된다.
  • (3)의 addEventListener는 내부에서 콜백 함수를 호출할 때 call 메소드의 첫 번째 인자에 addEventListener메소드의 this를 그대로 넘기도록 정의돼 있기 때문에 콜백 함수 내부에서의 this가 addEventListener를 호출한 주체인 HTML엘리먼트를 가리키게 된다.

콜백 함수는 함수다

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

메소드를 콜백 함수로 전달할 경우

let obj = {
  vals: [ 1, 2, 3 ],
  logValues: function(v, i){
    console.log(this, v, i);
  }
};
obj.logValues(1, 2); // { vals: [1, 2, 3], logValues: f } 1 2
[4, 5, 6].forEach(obj.logValues); // window { ... } 4 0
// window { ... } 5 1
// window { ... } 6 2
  • obj 객체의 logValues는 메소드로 정의됐다.
  • 7번째 줄에서는 이 메소드의 이름 앞에 점이 있으니 메소드로써(obj의) 호출한 것
  • 따라서 this는 obj를 가리키고, 인자로 넘어온 1, 2가 출력
  • 8번째 줄에서는 이 메소드를 forEach 함수의 콜백 함수로서 전달
  • obj를 this로 하는 메소드를 그대로 전달한 것이 아니라, obj.logVales가 가리키는 함수만 전달한 것
  • 이 함수는 메소드로서 호출할 때가 아닌 한 obj와의 직접적인 연관이 없어진다.
  • forEach에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하는 인자를 지정하지 않았으므로 함수 내부에서의 this는 전역객체를 바라보게 된다.

즉, 어떤 함수의 인자에 객체의 메소드를 전달하더라도 이는 결국 메소드가 아닌 함수일 뿐이다.

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

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

콜백 함수 내부의 this에 다른 값을 바인딩하는 방법

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

단 이제는 작성한 함수를 this를 이용해 다양한 상황에 재활용할 수 없게 되어버렸다..!

func 함수 재활용

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, 1500);
  • callback2에는 (obj1의 func를 복사한) obj2의 func를 실행한 결과를 담아 이를 콜백으로 사용했다.
  • callback3의 경우 obj1의 func를 실행하면서 this를 obj3가 되도록 지정해 이를 콜백으로 사용했다.

위의 코드를 실행해보면 실행 시점으로부터 1.5초 후에는 'obj2'가, 실행시점으로 2초 후에는 'obj3'이 출력된다.

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

콜백 지옥과 비동기 제어

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

비동기

비동기는 동기의 반대! 동기적인 코드는 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방식이다. 반대로 비동기적인 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어간다.
사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나, 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기한다거나, 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는 등, 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드이다.

일단 간단한 콜백지옥을 살펴보자

setTimeout(function(name) {
  var 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, '에스프레소');

각 콜백은 커피 이름을 전달하고 목록에 이름을 추가한다.
목적 달성에는 지장이 없지만 들여쓰기 수준이 과도하게 깊어졌을뿐더러 값이 전달되는 순서가 '아래에서 위로'향하고 있어 어색하게 느껴진다.

가독성 문제와 어색함을 동시에 해결하는 가장 간단한 방법은 익명의 콜백함수를 모두 기명함수로 전환하는 것이다.

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, '에스프레소');

이 방식은 코드의 가독성을 높일뿐 아니라 함수 선언과 함수 호출만 구분할 수 있다면 위에서부터 아래로 순서대로 읽어내려가는데 어려움이 없다. 또한 변수를 최상단으로 끌어올림으로써 외부에 노출되게 됐지만 전체를 즉시 실행 함수 등으로 감싸면 간단히 해결된다.
but, 일회성 함수를 전부 변수에 할당하는 것이 불편하게 느껴질수도 있다. 이를 보완하기 위해 promise, async/await를 쓸 수 있다.

Promise

new Promise(function(resolve) {
  setTimeout(function() {
    var name = '에스프레소';
    console.log(name);
    resolve(name);
  }, 500);
}).then(function(prevName) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      var name = prevName + ', 아메리카노';
      console.log(name);
      resolve(name);
    }, 500);
  });
}).then(function(prevName) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      var name = prevName + ', 카페모카';
      console.log(name);
      resolve(name);
    }, 500);
  });
}).then(function(prevName) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      var name = prevName + ', 카페라떼';
      console.log(name);
      resolve(name);
    }, 500);
  })
});
  • new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 다음(then) 또는 오류구문(catch)으로 넘어가지 않는다.

따라서 비동기 작업이 완료될 때 까지 비로소 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능하다.

Generator

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

let 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);
};
let coffeeMaker = coffeeGenerator();
coffeeMaker.next();

Generator 함수
6번째 줄의 "*"이 붙은 함수가 바로 Generator 함수이다.
Generator 함수를 실행하면 Iterator가 반환되는데, Iterator는 next라는 메소드를 가지고 있다.
이 next 메소드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다. 이후 다시 next 메소드를 호출하면 앞서 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield에서 함수의 실행을 멈춘다.
즉, 비동기 작업이 완료되는 시점마다 next 메소드를 호출해준다면, generator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행된다.

Promise + Async/await

var addCoffee = function(name) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      reslove(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();

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

정리

  • 콜백 함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.
  • 제어권을 넘겨받은 코드는 다음과 같은 제어권을 가진다.
  1. 콜백 함수를 호출하는 시점을 스스로 판단해서 실행한다.
  2. 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있다. 이 순서를 따르지 않고 코드를 작성하면 엉뚱한 결과를 얻게 된다.
  3. 콜백 함수의 this가 무엇을 바라보도록 할지가 정해져 있는 경우도 있다. 정하지 않은 경우에는 전역객체를 바라본다.
  4. 사용자가 임의로 this를 바꾸고 싶을 경우 bind 메소드를 활용하면 된다.
  5. 어떤 함수에 인자로 메소드를 전달하더라도 이는 결국 함수로서 실행된다.
  6. 비동기 제어를 위해 콜백 함수를 사용하다 보면 콜백 지옥에 빠지기 쉽다. 최근 나온 async/await 등을 적극 활용하자!
profile
Prof.Google을 통해 필요한 정보를 이 곳에 insert 🐸

0개의 댓글