[JS] 콜백함수

강은비·2022년 1월 3일
0

JS

목록 보기
12/19
post-custom-banner

코어 자바스크립트 책을 읽고 배운 내용을 바탕으로 작성되었다.


📌 콜백함수란?

callback function

  • 함수 또는 메서드의 매개변수로 전달되어 그 제어권도 함께 넘기는 함수이다.
  • 콜백함수를 전달받은 함수 또는 메서드는 자체적인 내부 로직에 의해 이 콜백함수를 적절한 시점에 호출한다.

📌 제어권

✨ 호출시점

setInterval

let intervalID = setInterval(func, [delay, arg1, arg2, ...]);
  • Window 또는 Worker 인스턴스가 제공하는 메서드
  • 일정한 시간 간격으로 콜백함수를 호출한다.
  • setInterval를 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값 반환하여 나중에 실행 내용을 clearInterval()을 호출하여 취소할 수 있다.
  • 첫번째 인자로 받은 콜백함수의 제어권은 setInterval에게 있고 콜백함수 호출 시점에 대한 제어권을 가진다.

✨ 인자

map

Array.prototype.map(callback[, thisArg])
callback: function(currentValue, index, array)

  • map 메서드는 첫 번째 인자로 callback 함수를 받고, 생략가능한 두 번째 인자로 콜백함수 내부에서 this로 인식할 대상을 지정할 수 있다. (thisArg를 생략할 경우, 콜백함수 내부의 this는 전역객체를 참조한다.)
  • 메서드의 대상이 되는 배열의 모든 요소들에 대해 콜백함수를 호출하고, 실행결과들을 모아 새로운 배열을 반환한다.
  • 이 때 콜백함수의 첫 번째 인자에는 배열 중 현재 값이, 두 번째 인자에는 현재 값의 인덱스가, 세 번째 인자에는 map 메서드의 대상이 되는 배열 자체가 담긴다.
  • 이는 map 메서드에 정의된 규칙으로 콜백함수의 제어권을 넘겨받은 코드는 콜백함수의 인자가 될 값들과 그 순서에 대한 제어권도 가진다.

✨ this

  • 콜백함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조하지만, 제어권을 넘겨받은 코드가 콜백함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.
  • setTimeout: 콜백함수 내부의 this가 전역객체를 가리킴.
  • forEach: 별도의 인자로 this를 넘겨주면 그 대상을 가리킴.
  • addEventListener: 콜백함수 내부의 this가 호출주체를 가리킴.


📌 콜백함수 내부의 this 바인딩

콜백함수도 함수이다.

  • 콜백함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 함수로서 호출된다.
  • 따라서 객체의 메서드를 콜백함수로 전달하면 해당 객체를 this로 바라볼 수 없다.

✨ 변수 이용

  • this를 변수에 담아 콜백함수로 활용할 함수에서는 this 대신 그 변수를 사용하게 함.
const obj1 = {
	name: 'obj1',
    func: function(){
    	const self = this;
        return function(){
        	console.log(self.name);
        };
    }
};
const callback = obj1.func();
setTimeout(callback, 1000);

✨ bind 메서드 활용

  • this가 다른 객체를 바라보도록 지정할 수 있어 코드를 재활용할 수 있다.
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);


📌 콜백지옥과 비동기 제어

✨ 비동기 vs 동기

asynchronous vs synchronous

  • 동기적인 코드: 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행한다.
    • CPU의 계산에 의해 즉시 처리가 가능한 대부분의 코드
  • 비동기적인 코드: 현재 실행 중인 코드의 완료 여부와 무관하게 바로 다음 코드가 실행된다.
    • 특정 시간이 경과되기 전까지 어떤 함수의 실행 보류 (setTimeout)
    • 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기 (addEventListener)
    • 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기 등 (XMLHttpRequest)
console.log("1st");
setTimeout(() => {
	console.log("2nd");
}, 0)
console.log("3rd");
  • 1st, 3rd, 2nd 순으로 콘솔에 출력된다.
  • 특정 로직의 실행이 끝날 때까지 기다리지 않고 나머지 코드를 먼저 실행하는 것이 비동기 처리이다.

비동기 처리가 필요한 이유

  • 자바스크립트는 싱글 스레드 기반의 동기적 언어이다.
    • 싱글 스레드: call stack이 하나인 것을 의미한다.
    • 즉, 한번에 하나의 작업만 할 수 있다.
  • 여러가지 이벤트를 처리할 때 동기적으로 하나씩 처리할 경우, 많은 시간이 소요되는 하나의 이벤트를 처리한다고 할 때 해당 작업이 끝날 때까지 다른 어떠한 작업도 처리할 수 없게 된다.
  • 이렇게 되면 이용자 입장에서 불편함을 느낄 수 있기에 비동기 처리가 필요하다.
  • 자바스크립트의 런타임인 웹브라우저에서 Web APIs (비동기 메소드), Callback Queue, Event Loop를 가지고 있기 때문에 자바스크립트 언어가 비동기적으로 실행될 수 있다.

콜백 함수와 비동기 처리

  • 시간이 많이 걸리는 작업들을 비동기로 구현할 수 있게 되었지만, 실행 결과의 순서가 보장되지 않는다.
  • 비동기 작업들이 순차적으로 진행되어야 하거나 비동기 작업을 실행하고 나온 결과값을 보장받기 위해 사용할 수 있는 가장 기본적인 방식이 콜백함수이다.
  • 예시: ajax 통신을 이용해 서버에서 데이터를 요청하여 받는 코드
    • $.get() 메소드는 비동기적 메소드이기 때문에 이 메소드의 작업이 완료되기 전에 다음 코드로 넘어가 undefined가 할당된 tableData가 콘솔에 출력된다.
function getData() {
    var tableData;
    $.get('https://domain.com/products/1', function (response) {
        tableData = response;
    });
    return tableData;
}
 
console.log(getData()); // undefined
  • 콜백함수를 이용한다면 비동기 작업을 순차적으로 처리하고 결과값을 얻을 수있다.
    • $.get()이 실행되고 나서 데이터가 수집되고 콜백함수를 호출해서 콜백함수의 매개변수에 데이터를 보내 콘솔에 출력한다.
function getData(callbackFunc) {
	$.get('https://domain.com/products/1', function(response) {
		callbackFunc(response);
	});
}

getData(function(tableData) {
	console.log(tableData);
});

✨ 콜백지옥

callback hell

  • 콜백함수만을 이용하여 비동기 코드를 동기식으로 작동하는 것처럼 코드를 작성하면 콜백지옥이 발생한다.
  • 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상으로, 자바스크립트에서 흔히 발생하는 문제이다.
  • 주로 이벤트 처리나 서버 통신과 같이 비동기적인 작업을 제어하기 위해 콜백지옥이 발생함.
  • 가독성이 떨어지고 코드 수정이 어렵다.
setTimeout(
  function (name) {
    let coffeeList = name;
    console.log(name);

    setTimeout(
      function (name) {
        coffeeList += ", " + name;
        console.log(name);

        setTimeout(
          function (name) {
            coffeeList += ", " + name;
            console.log(name);

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

✨ 콜백지옥 해결

❗ 기명함수로 변환

let 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(addEspresso, 500, "에스프레소");
  • 익명의 콜백 함수를 기명 함수로 변경하면 가독성도 좋아지고 들여쓰기도 없어진다.
  • 일회성 함수를 전부 변수에 할당해야 함.
  • 만약 커피가 100 종류가 있다면 100개의 함수를 만들어야 함.

❗ Promise

[JS] Promise

new Promise(function(resolve){
	setTimeout(function(){
    	const name = "에스프레소";
        console.log(name);
        resolve();
    }, 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);
    });
});
  • new 연산자와 함께 생성된 Promise의 인자로 넘긴 콜백함수 내부에서 resolve 또는 reject 함수를 호출한다.
  • 둘 중 하나가 호출되어야then 또는 catch 메서드로 넘어간다.
  • 따라서 비동기 작업이 완료될 때 resolve 또는 reject를 호출함으로써 비동기 작업의 동기적 표현이 가능하다.

❗ Generator

[JS] Generator

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

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

❗ Promise + async/await

[JS] Async/Await

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

const coffeeMaker = async function(){
	let 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();
  • 비동기 작업을 수행하고자 하는 함수 앞에 async 키워드를 붙이고, 함수 내부에서 실질적인 비동기 함수/메서드 앞에 await 키워드를 붙인다.


❗ 정리

  • 콜백 함수: 함수 또는 메서드의 메개변수로 전달되어 그 제어권도 함께 넘기는 함수이다.
  • 제어권을 넘겨받은 함수 또는 메서드는 다음과 같은 제어권을 갖는다.
    • 콜백함수를 호출하는 시점
    • 콜백함수를 호출할 때 인자로 넘겨줄 값들 및 순서
    • 콜백함수 내부의 this가 무엇을 바라보도록 할지 정해져 있는 경우도 있음. 정하지 않은 경우에는 전역객체를 바라봄. 사용자 임의로 this를 지정하고 싶을 때는 bind 메서드를 활용하면 된다.
  • 어떤 객체의 메서드를 콜백함수로 전달해도 결국 함수로서 실행된다.
  • 비동기 제어를 위해 콜백함수를 사용하다 보면 콜백지옥에 빠지기 쉽다. 최근의 ECMAScript에는 Promise, Generator, async/await 등 콜백 지옥에서 벗어날 수 있는 방법들을 제공한다.

참고

post-custom-banner

0개의 댓글