25. Chapter 2 프로그래밍 기초 2 (항해 5일차)

코이그·2023년 4월 7일

항해99

목록 보기
24/54

강의

콜백 함수

기본 개념

콜백 함수: 다른 코드의 인자로 넘겨주는 함수.
콜백 함수를 받는 주체가 있음. (forEach, setTimeout 등)

제어권이 그 주체들에 있음 (그 주체가 적절한 시점에 콜백 함수를 실행함).
스폰지밥과 알람시계의 예)
스폰지밥이 6시에 일어나려고 했을 때:

  1. 알람을 맞추지 않으면 스스로 일어나서 시간을 확인해야함. (주체: 스폰지밥)
  2. 알람을 맞춰놓으면 시간이 되면 시계가 알람을 울림. (주체: 알람시계(콜백함수))

제어권을 넘겨줄테니 너가 알고 있는 그 로직으로 처리해줘

// 콜백 함수 1
// 1초 후 (console.log) 실행.
setTimeout(function () {
  console.log("hello");
}, 1000);

// 콜백 함수 2
const numbers = [1, 2, 3, 4, 5];

// forEach가 알아서 로직을 처리
numbers.forEach(function (number) {
  console.log(number);
});

즉, 콜백 함수는 다른 코드(함수 또는 메소드)에 인자와 동시에 제어권도 넘겨주는 함수.

제어권

  1. 호출 시점
// setInterval :
//   반복해서 매개변수로 받은 콜백함수의 로직을 수행한다.
//   반환값: interval(간격)을 고유하게 식별할 수 있는 ID.
//   clearInterval 함수를 호출하여 제거할 수 있음.

var count = 0;

// 콜백 함수
var cbFunc = function () {
  console.log();
  // Interval을 초기화시켜주는 조건
  if (++count > 4) clearInterval(timer);
};

var timer = setInterval(cbFunc, 300);

// 실행 결과
// 0 (0.3sec)
// 1 (0.6sec)
// 2 (0.9sec)
// 3 (1.2sec)
// 4 (1.5sec)
  • cbFunc 호출: 호출 주체, 제어권은 user.
  • cbFunc를 setInterval로 넘김: 호출 주체, 제어권은 setInterval.
  1. 인자
// map: 배열의 요소를 순회하며 가공하여 새로운 배열을 반환
//   0번째 매개변수: 요소
//   1번째 매개변수: 요소의 인덱스

const arr = [10, 20, 30];
let newArr = arr.map(function (currentValue, index) {
  console.log(currentValue, index);
  return currentValue + 5;
});

console.log(newArr);
  • map, filter, reduce 등의 함수를 호출할 때는 정의된 규칙대로(콜백 내부 인자 포함) 작성해야 함.
  • 인자(의 순서)에 대한 제어권이 map 함수에 넘어감.

this binding

  • 콜백 함수도 함수이므로 기본적으로 this는 전역객체를 참조함.
  • 하지만 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조함 (map 함수 이용).

map 함수 구현

// map 함수는 첫 번째 인자로 콜백 함수, 두 번째 인자로 this를 받음.
Array.prototype.myMap = function (callback, thisArg) {
  // map 함수에서 return할 결과 배열
  var mappedArr = [];

  // 이 함수의 호출 주체는 [1, 2, 3] 배열이기 때문에 this는 배열.
  for (let i = 0; i < this.length; i++) {
    // this 바인딩을 위한 call 함수 호출
    // mappedValue에 this[i]을 할당
    // 콜백 함수 내부에서 this를 명시적으로 바인딩하기 때문에 가능
    let mappedValue = callback.call(thisArg || global, this[i]);
    mappedArr[i] = mappedValue;
  }
  return mappedArr;
};

console.log(
  [1, 2, 3].myMap(function (number) {
    return number * 2;
  })
);

콜백 함수로 객체의 메소드 전달

// obj
// 2가지 속성

var obj = {
  vals: [1, 2, 3],
  logValues: function (v, i) {
    console.log(">>> test starts");
    if (this !== global) console.log(this, v, i);
    console.log(">>> test ends");
  },
};

// method로서 호출
obj.logValues(1, 2);

console.log();

// obj에 있는 logValues 함수를 global에서 호출
// (위의 logValues의 function (~~~) 부분을 넣은 것과 같음)
[4, 5, 6].forEach(obj.logValues);

출력 결과:

>>> test starts
{ vals: [ 1, 2, 3 ], logValues: [Function: logValues] } 1 2
>>> test ends

>>> test starts
>>> test ends
>>> test starts
>>> test ends
>>> test starts
>>> test ends
  1. logValues가 메소드로써 호출이 되었기 때문에 위의 if문의 출력문이 실행된다.
  2. logValues 함수 자체가 for each의 매개변수로 전달되었기 때문에 메소드가 아닌 함수로 호출이 된다. this가 global이기 때문에 if문의 출력문은 실행되지 않는다.

콜백 함수를 매개변수로 넣을 때는 함수 자체(함수의 매개변수 없이)만 넣어주어야 한다

콜백 함수 내부에 원하는 this binding

bind 함수를 이용하여 this로 바인딩된 새로운 함수를 반환

var obj1 = {
 name: "obj1",
 func: function () {
   console.log(this.name);
 },
};
  
var obj3 = { name: "obj3" };
setTimeout(obj1.func.bind(obj3), 1000);
  • obj1의 func함수에 원하는 this인 obj3가 bind된 새로운 함수가 반환됨

콜백 지옥

콜백 지옥: 콜백 함수를 익명 함수(function ())로 전달하는 과정이 여러 번 반복되면 들여쓰기가 많아져 가독성도 떨어지고, 어떤 함수가 어떤 기능을 하는 지 이해하기 어렵고, 수정/유지보수 등이 어려워서 생긴 말.

콜백 지옥은 보통 비동기적인 로직에서 많이 생김.

동기? 비동기?

동기 vs. 비동기

예시)
카페에서 여러 명이 주문을 하려고 줄을 서있을 때
1. 한 사람 주문을 받고 그 사람의 주문을 완료할 때까지 다른 사람들은 대기하는 프로세스. 비효율적 (동기)
2. 한꺼번에 주문을 받고 주문에 따라 소요시간이 빠른 주문건부터 완료하는 프로세스. 효율적 (비동기)

동기 (Synchronous)

  • 현재 실행 중인 코드가 끝나야 다음 코드 실행.
  • 우리가 보통 생각하고 구현했던 코드가 동기적 코드.
  • 순서가 보장됨.

비동기 (Asynchronous)

  • 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어감.
    위 코드의 순서를 봤을 때 1이 실행이 된 후에 2가 실행될 것 같지만, 2가 먼저 바로 실행이 되고 2초 후에 1이 실행됨.
  • 비동기적 코드: 요청, 대기, 보류 등 (통신)
  • 순서가 보장되지 않음.

예) setTimeout.

// 비동기적 코드의 이해
setTimeout(function () {
  // 1
  console.log("여기가 먼저 실행될까?");
}, 2000);

// 2
console.log("아니면 여기?");

콜백 지옥 예시 & 해결 방안

예시)

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

출력 결과:

// 0.5초 후
에스프레소
// 0.5초 후
에스프레소, 아메리카노
// 0.5초 후
에스프레소, 아메리카노, 카페모카
// 0.5초 후
에스프레소, 아메리카노, 카페모카, 카페라떼
  • 위 코드는 가독성이 떨어져서 해석하기 쉽지 않다.

해결 방법 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, '에스프레소');
  • 익명 함수 대신 기명 함수로 먼저 정의를 한 다음에 setTimeout에 첫 번째 함수를 넣어주기.
  • 단점: 함수를 한 번만 호출하더라도 함수에 네이밍을 하는 것이 귀찮음.

비동기적 작업의 동기적 방법

setTimeout 환경이 아닌 다른 방법으로 비동기적 작업을 수행할 때 순서가 보장되지 않으므로 통신의 오류가 생길 수 있음. 따라서 비동기적 작업을 동기적으로(순서를 보장하는 것처럼) 보이게끔 표현/구현하는 것이 필요함.

3가지 방법: 1) Promise, 2). Generator, 3) async/await

Promise

'처리가 끝나면 알려달라'는 약속.

  • new 연산자로 호출한 Promise의 매개변수로 넘어가는 콜백은 바로 실행됨.

resolve: 성공
reject: 실패

처리 결과에 따라 then, catch문으로 오류사항들을 추적할 수 있음.

위 콜백 지옥을 Promise 방법으로 구현)

new Promise(function (resolve) {
  setTimeout(function () {
    var name = "에스프레소";
    console.log(name);
    // 1
    resolve(name);
  }, 500);
})
  // 2
  .then(function (prevName) {
    // 3
    return new Promise(function (resolve) {
      setTimeout(function () {
        // 4
        var name = (prevName += ", 아메리카노");
        console.log(name);
        // 1
        resolve(name);
      }, 500);
    });
  })
  // 2
  .then(function (prevName) {
    // 3
    return new Promise(function (resolve) {
      setTimeout(function () {
        // 4
        var name = (prevName += ", 카페모카");
        console.log(name);
        // 1
        resolve(name);
      }, 500);
    });
  })
  // 2
  .then(function (prevName) {
    // 3
    return new Promise(function (resolve) {
      setTimeout(function () {
        // 4
        var name = (prevName += ", 카페라떼");
        console.log(name);
        resolve(name);
      }, 500);
    });
  });
  1. resolve가 실행되기 전까지는 다음으로 넘어가지 않음.
  2. resolve가 실행되면 then으로 넘어감. resolve의 인자가 then의 인자(function())의 인자로 들어감.
  3. 새롭게 promise 생성 후 반환.
  4. 새롭게 생성된 promise에서 then으로 받아온 인자에 문자열 붙이기.

리팩토링된 코드)

// 1
var addCoffee = (name) => {
  return function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        // 2
        var newName = prevName ? `${prevName}, ${name}` : name;
        console.log(newName);
        // 3
        resolve(newName);
      }, 500);
    });
  };
};

// 4
addCoffee("에스프레소")()
  // 5
  .then(addCoffee("아메리카노"))
  .then(addCoffee("카페모카"))
  .then(addCoffee("카페라떼"));
  1. 리팩토링하기 전 코드의 반복되는 함수(then의 인자로 들어가는 함수)를 따로 정의.
  2. 최초 호출 시 prevName(resolve로 넘겨받은 인자)이 없으므로 newName에 name 저장.
  3. resolve 함수를 호출하여 실행의 종료를 알리고 newName을 넘김.
  4. addCoffee 함수를 최초 호출 시에는 new Promise부터 실행해야 하기 때문에 끝에 ()를 붙여줌으로 addCoffee의 전체 호출이 아닌 두 번째 return 호출.
  5. 이 때부터는 prevName(resolve로 넘겨받은 인자)이 있기 때문에 첫 번째 return 호출.
Generator

기본적으로 iterator 객체를 반환함.

  • next 메소드: 비동기 작업을 완료하면 이 메소드를 호출해 다음으로 넘어갈 수 있음.
  • yield: 순서를 제어하는 키워드. (함수에서 가장 처음 나오는 yield에서 작업을 멈추고 next 메소드가 호출되면 다음 yield까지 작업을 하다가 또 멈춤)
// 1
var addCoffee = function (prevName, name) {
  setTimeout(function () {
    // 6
    coffeeMaker.next(prevName ? prevName + ", " + name : name);
  }, 500);
};

// 2
var coffeeGenerator = function* () {
  // 5
  var espresso = yield addCoffee("", "에스프레소");
  console.log(espresso);
  // 7, 5
  var americano = yield addCoffee(espresso, "아메리카노");
  console.log(americano);
  // 7, 5
  var mocha = yield addCoffee(americano, "카페모카");
  console.log(mocha);
  // 7, 5
  var latte = yield addCoffee(mocha, "카페라떼");
  console.log(latte);
};

// 3
var coffeeMaker = coffeeGenerator();
// 4
coffeeMaker.next();
  1. Generator 함수 내부에서 호출할 addCofee 함수 정의.
  2. Generator 함수 정의 (*이 붙음)
  3. coffeeMaker는 iterator 객체
  4. next를 호출함으로 Generator 함수 내부로 들어감.
  5. yield에서 멈춤.
  6. next를 만나면 멈춘 시점부터
  7. 다음 yield까지 수행하고 멈춤.
async/await

async: 비동기
await: 기다리다

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

// 2
var coffeeMaker = async function () {
	var coffeeList = '';

	var _addCoffee = async function (name) {
		coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
	};
    
    // 3
	await _addCoffee('에스프레소');
	console.log(coffeeList);
	await _addCoffee('아메리카노');
	console.log(coffeeList);
	await _addCoffee('카페모카');
	console.log(coffeeList);
	await _addCoffee('카페라떼');
	console.log(coffeeList);
};
coffeeMaker();
  1. coffeeMaker 함수 내부에서 호출할 addCofee 함수 정의. (promise 반환)
  2. coffeeMaker 함수에 async 키워드를 붙여서 스코프 내에 await를 만났을 때 멈출 수 있도록 설정.
  3. promise를 반환하는 함수인 경우 await를 만나면 무조건 끝날 때까지 기다림.
profile
COYG🔴⚪

0개의 댓글