콜백 함수 (Callback)

Sangmin Na·2022년 4월 13일
0

JavaScript

목록 보기
7/7

이전에 학습한 내용을 복기하고자 이 글을 작성합니다. 개선해야 할 점들을 댓글로 남겨주시면 감사드리겠습니다.

🌐 콜백 함수란?

✔️ 콜백 함수는 다른 코드(함수 또는 메소드)의 인자로 넘겨주는 함수를 말한다
✔️ 매개변수를 넘겨 받은 함수는 callback 함수를 필요에 따라 즉시 실행(synchronously)할 수도 있고, 아니면 나중에 (asynchronously)실행할 수도 있다.

  • 다른 코드에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.
  • 콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행한다.

📌 동기 콜백 호출 vs 비동기 콜백 호출

// 동기적으로 3초 마다 배열안에 있는 수가 출력된다. (블로킹)
[1, 2, 3].forEach((num) => {
  // 콘솔 한 번에 매 3초가 걸린다.
  // 이 함수의 콜백 함수는 동기적으로 호출된다.
  callbackFn(() => console.log(num), 3000);
});

function callbackFn(callback) {
   const start = Date.now();
   let let = start;
  
   while ( now - start < 3000 ) {
      now = Date.now(); 
   }
  
    callback();
}

cosnole.log("ended");

// 콘솔 출력까지 총 수행 시간 3 * 3 = 9초
// 의도적으로 타이머의 기능이 필요하다면 동기적 콜백 호출이 필요할 수도 있다.
// 비동기적으로 3초 이후 배열의 수가 출력된다. (논 블로킹)
[1, 2, 3].forEach((num) => {
	// 자바스크립트 런타임에 존재하는 Web API에 setTiemout 함수를 사용함으로써 3초 이후 콘솔이 모두 출력된다.
  	// 이 함수의 콜백 함수는 비동기적으로 호출이 된다.
  setTimeout(() => console.log(num), 3000);
});

console.log("ended");

// 콘솔 출력까지 총 수행 시간은 3초
// 블로킹을 해결하고, 속도 개선을 위해선 비동기적 콜백 호출을 이용한다.
  • 여기서, 짚고 넘어가야할 것은 콜백 함수라고 해서 모두 비동기 함수가 아니라는 것이다.
  • 콜백 함수의 개념은 다른 코드의 인자로 함수를 넘겨주는 것을 의미한다는 것을 명심하자.

📌 비동기 주요 사례

  • 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나, 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기한다거나, 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는 등, 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드입니다.

👉 DOM 요소의 이벤트 핸들러

  • 마우스, 키보드 이벤트 (click, keydown 둥)
  • 페이지 로딩 (DOMContentLoaded 등)

👉 타이머

  • setTimeout, setInterval 등
  • 애니메이션 API (requestAnimationFrame)

👉 서버에 자원 요청

  • fetch API
  • AJAX

🌐 콜백 지옥과 비동기 제어

✔️ 콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상이다.

  • 주로 이벤트 처리나 서버 통신과 같이 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하곤 하는데, 가독성이 떨어질뿐더러 코드를 수정하기도 어렵다.

📌 (1-1) 콜백 지옥 예시

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);

		}, 500, "아메리카노");
	}, 500, "카페라떼");
}, 500, "카페모카");
  • 위 예제는 0.5초마다 커피 목록을 수집하고 출력한다. 기능에는 지장이 없지만 들여쓰기 수준도 과도하게 깊어지고 전달되는 매개변수의 순서도 아래에서 위로 향하고 있어 어색하게 느껴진다.
  • 이를 해결하는 방법은 여러가지가 있다. 우선은 Promise, async/await을 사용하지 않은 방법을 우선 한 번 살펴보자.

📌 (1-2) 콜백 지옥 해결 - 기명함수로 전환

let coffeeList = "";

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

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

const addAmericano = function (name) {
	coffeeList = name;
    console.log(coffeeList);
}

setTimeout(addMocha, 500, "카페모카");
  • 위와 같은 방식은 가독성을 높일뿐 아니라 함수 선언과 함수 호출만 구분할 수 있다면 위에서 아래로 순서대로 읽어내려가는 데 어려움이 없다.

  • 변수가 외부에 노출되기는 했지만 즉시 실행 함수로 감싸면 간단히 해결이 가능하다.

  • 자바스크립트는 비동기적인 방법은 동기적으로 보이게끔 처리해주는 노력을 꾸준히 해왔다. 위 방법은 하나의 역사로 참고해주면 좋다.

  • 최근에는, ES6에서 도입된 Promise, Generator와 ES2017에서 도입된 async/await이 가장 많이 사용된다.

📌 (2-1) 비동기 작업의 동기적 표현 Promise

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

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

📌 (2-2) 비동기 작업의 동기적 표현 Promise

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

addCoffee("카페모카")()
	.then(addCoffee("카페라떼"))
    .then(addCoffee("아메리카노"))
  • 위 방법은 반복적인 내용을 함수화 해서 더욱 짧게 표현한 것이다.

  • 2번째, 3번째줄은 클로저가 형성 되어있다.

📌 (3) 비동기 작업의 동기적 표현 Generator

const addCoffee = function (prevName, name) {
 	setTimeout(() => {
    	coffeeMaker.next(prevName ? `${prevName}, ${name}` : name);
    }, 500); 
};

const coffeeGenerator = function* () {
 	const mocha = yield addCoffee("", "카페모카");
	console.log(mocha);
  	const latte = yield addCoffee(mocha, "카페라떼")
    console.log(latte);
  	const americano = yield addCoffee(latte, "아메리카노");
  console.log(americano);
}

const coffeeMaker = coffeeGenerator();
coffeeMaker.next();
  • 함수 뒤에 "*"이 붙은 함수가 바로 Generator함수이다.

  • Generator 함수를 실행하면 Iterator가 반환되는데, next라는 메소드를 가지고 있다.

  • next 메소드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다.

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

  • 비동기 작업이 완료되는 시점마다 next 메소드를 호출하면 Gnerator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행된다.

📌 (4) 비동기 작업의 동기적 표현 Promise + Async/Await

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

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

  • 즉 Promise의 then과 흡사한 효과를 얻을 수 있다.

🌐 정리

  • 콜백 함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.

  • 제어권을 넘겨받은 코드는 다음과 같은 제어권을 가진다.

    • 1) 콜백 함수를 호출하는 시점을 스스로 판단해서 실행한다.

    • 2) 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있다.

    • 3) 콜백 함수의 this가 무엇을 바라보도록 할지 정해야 하는 경우도 있다. 정해지지 않은 경우엔 전역 객체르 바라보고, 임의로 변경하고 싶다면 bind 메서드를 사용하면 된다.

  • 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행된다.

  • Promise, Generator, Async/Await 등을 이용하여 콜백 지옥을 어느정도 해결할 수 있다.

Reference

0개의 댓글