본문 바로가기
  • GDG on campus Ewha Tech Blog
4-1기 스터디/Javascript-Typescript Deep Dive

[JS Deep Dive 스터디] 4주차 - 콜백 함수

by 호프 2023. 1. 25.

일시: 2022.11.20

참석자:


금주 스터디는 조민재님께서 발표해주셨습니다.

04. 콜백함수

콜백 함수란?

  • 다른 코드의 인자로 넘겨주는 함수
  • 콜백 함수를 넘겨받은 코드는 해당 함수를 필요에 따라 적절히 실행
  • 어떤 함수 X를 호출하면서 '특정 조건일 때 함수 Y를 실행해서 나에게 알려달라'는 요청을 함께 보내는 것
  • 이 요청을 받은 함수 X의 입장에서는 해당 조건이 갖추어 졌는지 여부를 스스로 판단하고 Y를 직접 호출
  • 다른 코드(함수 / 메서드)에게 인자를 넘겨줌으로써 그 제어권도 함께 위임한 함수
  • 콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 콜백 함수를 적절한 시점에 실행

제어권

  • 호출 시점
  • var count = 0; var timer = setInterval(function(){ //setInterval을 실행한 결과 할당 console.log(count); if(++count > 4) clearInterval(timer); }, 300) //2개의 매개변수 전달 (익명함수, 숫자)
  • setInterval의 구조
    var IntervalID = scope.setInterval(func, delay[ ,param1, param2, ...]);
    • scope에는 window 객체 또는 worker의 인스턴스가 들어올 수 있음, 두 객체 모두 setInterval 메소드를 제공하기 대문에 일반적인 브라우저 환경에서는 window 생략해서 함수처럼 사용 가능한 것
    • func, delay 값은 반드시 전달 / 3번째 매개변수부터는 선택적
    • func : 함수 / delay : 밀리초 단위의 숫자
    • func에 넘겨준 함수는 매 delay 마다 실행되고, 결과로 아무것도 리턴하지 않음
    • setInterval 실행 시 반복적으로 실행되는 내용 자체를 특정할 수 있는 ID 값이 반환됨
    • 이를 변수에 담는 이유는 반복 실행되는 중간에 종료(clearInterval)를 위한 것 (ID를 통해 종료 시점 이후 사이클을 삭제)
    var count = 0; 
    var cbFunc = function(){ 
      console.log(count); 
      if(++count > 4) clearInterval(timer); 
    };
    var timer = setInterval(cbFunc, 300);  
  • cbFunc 함수가 콜백 함수 (제어권을 setInterval 함수에 위임한 콜백 함수)
code 호출 주체 제어권
cbFunc(); 사용자 사용자
setInterval(cbFunc, 300); setInterval setInterval

인자

  • 콜백함수 예제코드 11) currentValue 10, index 0 의 값 출력 후 15 (10+5) 반환
    2) currentValue 20, index 1 의 값 출력 후 25 (20+5) 반환
    3) currentValue 30, index 2 의 값 출력 후 35 (30+5) 반환
    4) [15,25,35] 라는 새로운 배열이 만들어져서 변수 newArr에 담기고 출력
  • var newArr = [10,20,30].map(function (currentValue, index){ console.log(currentValue, index); return currentValue + 5; }); console.log(newArr); //실행 결과 //10 0 //20 1 //30 2 //[15,25,35]
  • map 메소드의 구조(1)
    • 첫번째 인자로 callback 함수 받음
    • 생략 가능한 두번째 인자로 콜백 함수 내부에서 this로 인식할 대상 특정 가능
    • thisArg 생략할 경우 전역객체가 바인딩
    • map 메소드는 메소드의 대상이 되는 배열의 모든 요소를 처음부터 끝까지 하나씩 꺼내어 콜백 함수를 반복 호출하고, 콜백 함수의 실행 결과를 모아 새로운 배열을 만들어냄
    • (2)
    • 콜백 함수의 첫 번째 인자에는 배열의 요소 중 현재값, 두번째 인자에는 현재값의 인덱스, 세번째 인자에는 map 메소드의 대상이 되는 배열 자체가 담김
  • Aray.prototype.map(callback[ , thisArg]) //(1) callback: function(currentValue, index, array); //(2)
  • 콜백함수 예제코드 2
    • map 메소드를 제이쿼리 방식으로 바꿈
    • 제이쿼리 메소드는 첫번째 인자에 index, 두번째 인자에 currentValue가 옴
      var newArr2 = [10,20,30].map(function (index ,currentValue){
      console.log(index, currentValue); 
      return currentValue + 5; 
      }); 
      console.log(newArr); 
      //실행 결과
      //10 0
      //20 1
      //30 2
      //[5,6,7]

this

  • "3장 중: 콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조하지만 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다."
  • this를 지정하는 방식 및 제어권에 대한 이해를 돕는 map 메서드 구현 코드메소드 구현의 핵심은 call / apply 메소드에 있음
    this에는 thisArg 값이 있으면 해당 값을, 없을 경우 전역 객체를 지정
    첫번째 인자에는 메솓의 this가 배열을 가리키므로 배열의 i번째 요소 값을, 두번째 인자에는 i 값을, 세번째 인자에는 배열 자체를 지정해 호출
    그 결과 변수 mappedValue에 담겨 mappedArr의 i번째 인자에 할당
    정리 > this에 다른 값이 담기는 이유:
    제어권을 넘겨받은 코드에서 call/apply 메소드의 첫 번째 인자에 콜백함수 내부에서의 this가 될 대상을 명시적으로 바인딩하기 때문
    • 콜백 함수 내부에서의 this
      setTimeout(function() {console.log(this); }, 300); //(1)
      
    [1,2,3,4,5].forEach(function(x){});
    console.log(this,e);   //(3)
    })
  • (1) setTimeout은 내부에서 콜백함수 호출 시 call 메소드의 첫번째 인자에 전역 객체를 넘기므로 콜백 함수 내부에서의 this가 전역객체를 가리킴 (2) forEach는 '별도의 인자로 this를 받는 경우'에 해당하지만 별도의 인자로 this를 넘겨주지 않았기 때문에 전역 객체를 가리킴 (3) addEventListener는 내부에서 콜백 함수 호출 시 call 메소드의 첫 번째 인자에 addEventListener 메소드의 this를 넘기도록 정의되어 있기 때문에 콜백 함수 내부에서의 this가 addEventListener를 호출한 주체인 HTML 엘리먼트를 가리킴
  • document.body.innerHTML += '
  • console.log(this); //(2)
  • Array.prototype.map = function(callback, thisArg){ var mappedArr = []; for ( var i=0; i < this.length; i++){ var mappedValue = callback.call(thisArg || window, this[i], i, this); mappedArr[i] = mappedValue; } return mappedArr; }

콜백 함수는 함수다

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

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

    • 객체의 메소드를 콜백 함수로 전달하면 해당 객체를 this로 바라볼 수 없게 됨
    • 콜백 함수 내부의 this에 다른 값을 바인딩하는 방법
      • 전통적인 방식
        var obj1 = {
          name: 'obj1', 
          func: function(){
              var self = this; 
              return function (){
                  console.log(self.name); 
              }; 
          }
        }; 
        var callback = obj1.func(); 
        setTimeout(callback, 1000); 
        obj.func 메소드 내부에서 self 변수에 this를 담고, 익명 함수를 선언함과 동시에 반환
        obj1.func을 호출하면 앞서 선언한 내부함수가 반환되어 callback 변수에 담김
        callback을 setTimeout 함수에 인자로 전달하면 1초 뒤 callback이 실행되면서 obj1이 출력됩니다.
        • 콜백 함수 내부에서 this를 사용하지 않은 경우
          var obj1 = {
          name: 'obj1', 
          func: function(){
                 console.log(obj1.name); 
           }
          }; 
          setTimeout(obj1.func, 1000); 
          • 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, 2000);

콜백 지옥과 비동기 제어

  •  
  • callback2에 obj2의 func를 실행한 결과를 담아서 콜백으로 사용 callback3의 경우 obj1의 func을 실행하면서 this를 obj3가 되도록 지정해서 콜백으로 사용 this를 우회적으로 활용 - bind 메소드 활용 (ES5) ```javascript var obj1 = { name: 'obj1', func: function(){ console.log(this.name); } }; setTimeout(obj1.func.bind(obj1), 1000); var obj2 = {name: 'obj2'}; setTimeout(obj1.func.bind(obj2), 1500);
  • 콜백 지옥이란?
    콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상
    주로 이벤트 처리, 서버 통신과 같이 비동기적인 작업을 수행하기 위해 이러한 형태가 자주 등장
  • 비동기란?
    동기의 반대말, 동기적인 코드는 현재 실행 중인 코드가 완료된 후 다음 코드를 실행하는 방식
    비동기적인 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어감
    CPU 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적인 코드
  • 별도의 요청, 실행 대기, 보류* 등과 관련된 코드는 비동기적 코드
    • 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류 (setTimeout)
    • 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기(addEventListener)
    • 웹 브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기(XMLHttpRequest)
  • 콜백 지옥 예시
  • 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, '에스프레소' );

/*결과

"에스프레소"
"에스프레소,아메리카노"
"에스프레소,아메리카노,카페모카"
"에스프레소,아메리카노,카페모카,카페라떼"

*/

-  콜백 지옥 해결: 기명함수로 변환
```javascript
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, '에스프레소'); 
  • 비동기적 작업의 동기적 표현 - Promise(1)
    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); 
     })
    })
    ES6의 Promise를 이용한 방식
    new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then 또는 catch 구문으로 넘어가지 않음
    따라서 비동기 작업이 완료될 때 비로소 resolve, reject를 호출하는 방법으로 비동기 작업의 동기적 표현을 가능하게 함
  • 비동기적 작업의 동기적 표현 - Promise(2)
    var addCoffee = function(name){
     return function (prevName){ //클로저 
         return new Promise(function(resolve){ //클로저
         setTimeout(function(){
             var name = prevName ? (prevName + ',' + name) : name; 
             console.log(name); 
             resolve(name); 
         },500); 
      });
    };
    };
    

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


-  비동기적 작업의 동기적 표현 - Generator
```javascript
var addCoffee = function(preName, name){
    setTimeout(function(){
           coffeMaker.next(prevName ? prevName + ',' + name : name); 
        },500); 
    };
var coffeeGenerator = function* (){
    var espresso = yield addCoffee('', '에스프레소'); 
    console.log(espresso); 
    var americano = yield addCoffee(espresso, '아메리카노'); 
    console.log(americano); 
    var mocha = yield addCoffee(americano, '아메리카노'); 
    console.log(americano); 
    var latte = yield addCoffee(mocha, '아메리카노'); 
    console.log(latte); 
}; 
var coffeeMaker = coffeeGenerator(); 
coffeeMaker.next(); 

'*'이 붙은 함수가 Generator 함수
함수 실행 시 Iterator가 반환되는데 Iterator는 next 라는 메소드를 가지고 있음
이 next 메소드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춤
이후 다시 next 메소드를 호출하면 앞서 멈췄던 부분부터 다음 yield에서 실행을 멈춤
비동기 작업이 완료되는 시점마다 next 메소드를 호출해주면 동기적 진행을 표현할 수 있음

    • 비동기적 작업의 동기적 표현 - Promise + Async/await
      var addCoffee = function(name){
       return new Promise(function (resolve){
           setTimeout(function(){
               resolve(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(); 
      ES2017 의 Async/await
      비동기 작업을 수행하고자 하는 함수 앞에 async 를 표기
      함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기
    •  

뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후 다음으로 진행함
즉 Promise의 then과 흡사한 효과를 얻을 수 있음

  •  

댓글