[TIL] 콜백 함수

Kyoorim LEE·2023년 8월 15일
0

스스로TIL

목록 보기
26/34

모던자바스크립트 - 콜백함수

콜백

콜백(callback)은 '되돌아 호출해달라'는 명령이다. 즉 어떤 함수 X를 호출하면서 '특정 조건일 때 함수 Y를 실행해서 나에게 알려달라'는 요청을 함께 보내는 것. 이 요청을 받은 함수 X는 해당 조건이 갖춰졌는지 스스로 판단하고 Y를 직접 호출한다.

콜백함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다. 콜백함수를 위임받은 코드는 자체적인 내부 로직으로 콜백함수를 적절한 시점에 시행한다.콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.

비동기(asynchronous) 동작은 원하는 때에 동작을 시작하도록 할 수 있으며 setTimeout이 대표적인 예시다. 실무에서 사용하는 비동기 동작은 매우 다양하다(ex. 스크립트나 모듈 로딩)

src에 있는 스크립트를 읽어오는 함수 loadScript(src)를 예시로 살펴보자

function loadScript(src) {
  // <script> 태그를 만들고 페이지에 태그를 추가
  // 태그가 페이지에 추가되면 src에 있는 스크립트 로딩 및 실행합
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

// 해당 경로에 위치한 스크립트 불러오고 실행하기
loadScript('/my/script.js');// script.js엔 "function newFunction() {…}"이 있다

newFunction()

함수 loadScript(src)<script src="…">를 동적으로 만들고 이를 문서에 추가한다. 브라우저는 자동으로 태그에 있는 스크립트를 불러오고, 로딩이 완료되면 스크립트를 실행한다.

여기서 스크립트는 '비동기적으로' 실행된다. 로딩은 지금 시작되더라도 실행은 함수가 끝난 후에 되기 때문이다. 따라서 loadScript('/my/script.js') 아래 코드들은 스크립트 로딩이 종료되는 걸 기다리지 않는다.

스크립트 안에 다양한 함수가 정의되어있다고 할 때 loadScript(..)를 호출하자마자 내부 함수를 호출하면 원하는대로 작동하지 않는다. 따라서 loadScript에서 스크립트 로딩이 완료되었는지 여부를 알 수 있어야 한다.

loadScript의 두 번째 인수로 스크립트 로딩이 끝난 후 실행될 함수인 콜백함수를 추가해보자

콜백 함수: 나중에 호출할 함수, 콜백 함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 시행하게 됨

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

이렇게 두 번째 인수로 전달된 함수(대개 익명 함수)는 원하는 동작(외부 스크립트 불러오기)이 완료되었을 때 실행된다.

이런 방식을 ‘콜백 기반(callback-based)’ 비동기 프로그래밍이라고 한다. 무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 반드시 제공해야 한다.


콜백 속 콜백

두 개의 스크립트를 순차적으로 불러오고 싶을 때는 어떻게 해야할까? 가장 자연스러운 방법은 아래와 같이 콜백 함수 안에서 두 번째 loadScript를 호출하는 것이다.

loadScript('/my/script.js', function(script) {

  alert(`${script.src}을 로딩완료. 다음 스크립트 로딩 시작.`);

  loadScript('/my/script2.js', function(script) {
    alert(`두 번째 스크립트 로딩 성공.`);
  });

});

그러나 만약 여기에 스크립트 개수가 추가된다면 계속 중첩을 시켜야한다.

// 콜백 지옥
loadScript('/my/script.js', function(script) {

  loadScript('/my/script2.js', function(script) {

    loadScript('/my/script3.js', function(script) {
    
          loadScript('/my/script4.js', function(script) {
      //.....
    });
      
    });

  })

});

콜백 지옥

콜백 함수를 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상 => 가독성이 떨어지고 코드 수정도 어려움

콜백 지옥의 또 다른 예시

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

1. 콜백지옥 해결방법: 기명함수로 전환하기

가독성 문제와 어색함을 해결하기 위한 방법으로 익명의 콜백 함수를 모두 기명 함수로 전환할 수 있다.

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, "에스프레소");
  • 단점: 일회성 함수를 전부 변수에 할당해야 하는지 의문. 코드명을 일일이 따라다녀야 하므로 헷갈릴 수 있음.

2. 콜백지옥 해결방법: Promise (1) - resolve, reject

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

콜백지옥 해결방법: Promise(2) - then

반복적인 내용을 함수화해서 짧게 표현

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

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

콜백지옥 해결방법: Promise + Async/await

함수 앞에 async 키워드를 추가하면 함수는 언제나 Promise를 반환하며 함수 안에서 await를 사용할 수 있다.

비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고 함수 내부에서 실질적 비동기 작업이 필요한 위치마다 await를 표기. Promise의 then과 흡사한 효과를 얻을 수 있음

자바스크립는 await 키워드를 만나면 Promise가 처리될 때까지 기다린다. 결과는 그 이후 반환된다. Promise가 처리되길 기다리는 동안 엔진이 다른 일을 할 수 있으므로 CPU 리소스가 낭비되지 않는다.

awaitpromise.then보다 좀 더 세련되게 프라미스의 result 값을 얻을 수 있도록 해주는 문법이다. promise.then보다 가독성 좋고 쓰기도 쉽다.

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

var 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();
profile
oneThing

1개의 댓글

comment-user-thumbnail
2023년 8월 15일

큰 도움이 되었습니다, 감사합니다.

답글 달기