TIL57.콜백함수

조연정·2020년 12월 23일
0
post-thumbnail

'코어 자바스크립트' 책을 통해 콜백함수에 대해 깊게 알아보자.(챕터4)

콜백함수

콜백 함수는 다른 코드(함수,메소드)에 인자로 넘겨주는 함수로써, 인자로 넘기면서 동시에 '제어권' 도 함께 위임하게 된다.
다른 객체에게 일을 시키고, 그 일이 끝나는 것을 기다리는 것이 아니라 그 객체가 나를 다시 부를 때까지 내 할 일을 하고 있는 것이다.

콜백함수를 사용하는 이유

콜백함수가 함수의 인자로 전달되는 함수이며, 전달받은 함수 내에서 실행된다는 것은 알지만, 왜 콜백함수를 사용하는 것일까?

function getData() {
	var tableData;
	$.get('https://domain.com/products/1', function (response) {
		tableData = response;
	});
	return tableData;
}

console.log(getData());
// undefined

화면에 표시할 데이터를 서버에서 불러올 때, ajax통신으로 해당 데이터를 서버로부터 가져올 수 있다.
서버에 HTTP GET 요청을 날려 1번 product정보를 요청하는 코드이다. 서버에서 받아온 데이터는 response 인자에 담기고,tableData = response; 코드로 받아온 데이터를 tableData라는 변수에 저장한다. getData()를 호출하면 undefined가 나온다.
$get이 비동기적 메서드이어서 데이터를 요청하고 받아올 때까지 기다려주는 것이 아니라 다음코드인 return tableData를 실행하기 때문이다. 즉, tableData에는 아무런 값도 할당되지 않은 채로 함수가 끝난다.
이때 콜백함수를 사용하면 문제를 해결할 수 있다.

function getData(callbackFunc) {
    $.get('https://domain.com/products/1', function (response) {
        callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
    });
}
 
getData(function (tableData) {
    console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});

getData함수의 인자로 callback함수를 넣어주었다. 그 콜백함수는 $.get메소드 안에서 실행되었다. 서버에서 받은 데이터를 콜백함수의 인자로 넘겨준다.
콜백함수를 통해 비동기적 처리문제를 해결할 수 있게 된 것이다.

제어권

1.호출시점 제어

콜백함수를 인자로 사용하는 코드는 호출 시점에 대한 제어권을 가진다.

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

let timer = setInterval(cbFunc, 300);
//0~4까지 순서대로 1씩 증가한다.

콜백함수를 인자로 갖는 대표적인 함수 setInterval함수는 첫번째 인자로 cbfunc함수를 넘겨받았고, 두번째 인자는 얼마 후에 cbfunc 함수를 실행할거라는 호출 시점에 대한 제어권을 행사할 수 있다.

2.인자 제어

콜백함수를 인자로 사용하는 코드는 인자에 대한 제어권을 가진다.

let newArr = [10,20,30].map((currentVal, index)=> {
  console.log(currentVal, index);
  return currentVal + 5;
})
console.log(newArr)

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

// 1번인자와 2번인자를 바꾼 코드
let newArr = [10,20,30].map((index, currentVal)=> {
  console.log(index, currentVal);
  return currentVal + 5;
})
console.log(newArr)

10 0
20 1
30 2
[ 5, 6, 7 ]

map 메소드는 첫번째 인자를 콜백으로 받는 메소드이고, 그 콜백함수의 첫번째인자는 배열의 현재값, 두번째는 인덱스값이다. 위에 코드처럼 인자 순서를 서로 바꿨다고해서 같은 결과가 나오진 않는다. 이 결과는 콜백함수를 넘겨받은 map함수가 인자의 순서를 어떻게 받을 것인지 결정하는 인자의 제어권을 가지고 있다는 것을 보여준다.

3) This 제어

기본적으로 함수에서 this는 전역객체를 참조한다. 즉, window를 참조한다.
콜백함수도 함수이기때문에 이 규칙에 따르면, window를 참조한다는 뜻이다.
this를 윈도우가 아닌 다른 객체로 바꿔주기 위해서는 바인딩이 필요한데, 대표적인 바인딩 함수 call, apply,bind를 사용하면 된다.

//바인딩하지 않은 코드
let GymClassObj = {
  name: "몽",
  time: "오전 9시",
  showHealth: function () {
    setTimeout(
      function () {
        console.log(
          this.name + "님, 오늘은 " + this.time + "에 운동을 하셨네요"
        );
      },
      500
    );
  },
};

//바인딩한 코드
setTimeout(
      function () {
        console.log(
          this.name + "님, 오늘은 " + this.time + "에 운동을 하셨네요"
        );
      }.bind(this), //바인딩
      500
    );
// GymClassObj.showHealth();

바인딩하지 않은 코드의 결과는 "님, 오늘은 undefined에 운동을 하셨네요" 가 나온다. 바인딩을 하지 않았기때문에 this는 window를 참조해서 언디바인드를 도출한다.
원하는 결과값을 위해서는 setTimeout의 첫번째 인자 콜백함수에 bind()처리를 해주면 된다. => "몽님, 오늘은 오전 9시에 운동을 하셨네요"

콜백지옥

콜백함수를 익명함수로 전달하는 과정이 중첩됭 코드의 가독성이 떨어지는 현상을 말한다.
주로 이벤트 처리, 서버 통신과 같이 비동기적인 작업을 수행할 때 이러한 문제들이 발생한다.
ex)함수의 실행을 딜레이시키는 'setTimeout', 사용자의 직접적인 개입이 있을 때 함수를 실행하는 'addEventListener'

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

setTimeout이라는 함수 안에 첫번째 인자로 콜백함수가 들어가있는 콜백함수가 있고, 두번째 인자로 딜레이시간, 세번째로 콜백함수 인자에 전달하고 싶은 인자.
위의 코드를 보면, 인자값이 아래에서 위로 전해지는 모양새가 어색하다. 콜백 안에 콜백이 있는 모습도 상당히 지저분하다.

Promise

콜백지옥의 문제점을 해결하고자, promise가 도입되었다.

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

프로미스는 클래스이기때문에 new라는 키워드를 이용해서 오브젝트를 생성할 수 있고, 프로미스 생성자의 인자로 콜백함수를 받아야 한다. 또 그 콜백함수는 또 다른 콜백함수 2가지를 인자로 받을 수 있는데, resolve, reject가 있다.
resolve는 기능이 정상적으로 수행해서 최종데이터를 전달할 때 사용하고, reject는 기능을 불러오는데 실패하면 사용한다.
resolve함수가 불러오는데 성공하면, .then이 기다렸다가 리졸브함수에서 받은 인자값을 then의 인자로 가져온다.
함수를 불러오는데 실패하면 reject함수가 실행되고, .catch가 실행된다.

async, await

프로미스도 너무 많이 사용하게 되면 프로미스 체이닝이 되어 프로미스 지옥을 만들게 된다. 여기서 더 발전되어 나온 것이 async, await이다.

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

let coffeeMaker = async function () {
  let coffeeLsit = "";
  let _addCoffee = async function (name) {
    coffeeLsit += (coffeeLsit ? "," : "") + (await addCoffee(name));
  };
  await _addCoffee("에스프레소");
  console.log(coffeeLsit);
  await _addCoffee("아메리카노");
  console.log(coffeeLsit);
  await _addCoffee("카페모카");
  console.log(coffeeLsit);
  await _addCoffee("카페라떼");
  console.log(coffeeLsit);
};

coffeeMaker();

비동기 처리가 필요한 함수 앞에 async를 써주고, 비동기 처리 코드가 필요한 곳에 await를 써준다.promise에서 then의 개념이 await이다.

profile
Lv.1🌷

0개의 댓글