8/14 js 문법 종합 4주차

성준호·2024년 8월 14일
0
  • 콜백함수: 다른 코드의 인자로 넘겨주는 함수 제어권도 함께 위임
  1. 호출 시점 제어권
// 제어권
// 1. 호출 시점에 대한 제어권을 갖는다
// setInterval: 반복해서 매개변수로 받은 콜백함수의 로직을 수행
var count = 0;
var cbFunc = function () {
  console.log(count);
  if (++count > 4) clearInterval(timer);
};

var timer = setInterval(cbFunc, 300);

0, 1, 2, 3, 4 => setInterval이 호출 시점 제어권을 가져 300ms마다 실행함

  1. 인자 제어권
// 제어권
// 인자에 대한 제어권을 갖는다
// map 함수

// index, currentValue: 사람이 이해할 수 있는 변수명일 뿐
var newArr = [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 ]
=> index와 currentValue 순서를 바꾸었을 때 의도하지 않은 값이 나온다.
map 메서드가 인자 제어권을 갖기 때문에 정의된 규칙대로 작성해야 한다
map(현재 배열 요소, 인덱스, 호출한 배열)

  1. this
    콜백함수도 함수이기 때문에 기본적으로 this가 전역 객체를 참조한다.
    예외로 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우 그 대상을 참조한다.
Array.prototype.map123 = function (callback, thisArg) {
  // map 함수에서 return할 결과 배열
  var mappedArr = [];

  for (var i = 0; i < this.length; i++) {
    var mappedValue = callback.call(thisArg || globalThis, this[i]);
    mappedArr[i] = mappedValue;
  }

  return mappedArr;
};

var newArr = [1, 2, 3].map123(function (number) {
  return (number = 2);
});

console.log(newArr);

[2, 2, 2]
=> for (var i = 0; i < this.length; i++)
여기서 this는 map123 메서드를 호출한 배열 [1, 2, 3]을 참조한다.
var mappedValue = callback.call(thisArg || globalThis, this[i]);
callback 함수를 실행할 때 this가 어떤 객체를 참조할지 설정하고있다. 밑에서 newArr이 콜백함수만 전달하고 thisArg 인자는 전달하지 않고 있기 때문에 globalThis가 참조된다. 그리고 this[i]는 메서드를 호출한 배열 [1, 2, 3]을 참조한다. 이는 콜백함수의 number 인자로 들어간다. 따라서 배열의 i번째 인덱스에 콜백함수를 호출하여 반환된 값 number = 2가 할당되어 [2, 2, 2]가 출력된다.

  1. 콜백 함수는 함수다.
    콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 함수로서 호출한다
var obj = {
  vals: [1, 2, 3],
  logValues: function (v, i) {
    console.log(">>> test starts");
    if (this === global) {
      console.log("this가 global입니다 원하지 않는 결과!");
    } else {
      console.log(this, v, i);
    }
    console.log(">>> test ends");
  },
};

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

// forEach, map, filter
[4, 5, 6].forEach(obj.logValues);

test starts
{ vals: [ 1, 2, 3 ], logValues: [Function: logValues] } 1 2
test ends
test starts
this가 global입니다 원하지 않는 결과!
test ends
test starts
this가 global입니다 원하지 않는 결과!
test ends
test starts
this가 global입니다 원하지 않는 결과!
test ends
=> obj.logValue(1,2)로 메소드 호출. 이때 this가 참조하는 것은 obj이므로 if 조건을 거짓이 되어 else 구문이 실행된다. 따라서 obj 객체, 1, 2가 출력된다.
[4, 5, 6].forEach(obj.logValues) -> [4, 5, 6] 배열이 forEach 메소드를 호출. 이때 콜백함수로 obj.logValues를 전달하였다. 이는 메소드로 보이지만 obj.logValues가 가리키는 함수만 전달하는 것으로 this가 obj를 참조하지 않고 전역객체를 참조한다. 따라서 if문의 조건이 참이다.

  1. 콜백 함수 내부 this에 다른 값 바인딩
var obj1 = {
  name: "obj1",
  func: function () {
    var self = this; //이 부분!
    return function () {
      console.log(self.name);
    };
  },
};
var callback = obj1.func();
setTimeout(callback, 1000);

obj1
=> self 변수에 미리 this를 할당하여 사용한다. 여기서 this는 obj1을 참조한다. 함수를 리턴했을 때 클로저에 의해 리턴된 함수의 self도 동일하게 obj1을 가리킨다. 반환된 함수를 setTimeout의 콜백함수로 전달하면 obj1 출력.

4-1 call 활용

var obj3 = { name: "obj3" };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);

obj3
=> call 메소드로 obj1.func 메소드를 실행하되, this 참조를 obj3로 지정한다.

4-2 bind 활용

// bind
var obj1 = {
  name: "obj1",
  func: function () {
    console.log(this.name);
  },
};

var boundObj1 = obj1.func.bind(obj1);
setTimeout(boundObj1, 1000);

var obj2 = { name: "obj2" };
setTimeout(obj1.func.bind(obj2), 1500);

obj1, obj2
=> bind 메소드로 this가 참조할 객체를 지정하여 새로운 함수를 반환할 수 있다.

  • 동기와 비동기의 개념
    동기: 현재 실행 중인 코드가 끝나야 다음 코드를 실행
    비동기: 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드 실행
// 비동기적 코드의 이해
setTimeout(function () {
  console.log("여기가 먼저 실행될까?!?!");
}, 1000);

console.log("여기좀 봐주세요!!!");

여기좀 봐주세요!!!, 여기가 먼저 실행될까?!?!
setTimeout은 대표적인 비동기적 코드로 1000ms의 딜레이로 아직 콜백 함수가 실행되지 않았음에도 바로 다음 코드가 실행되는 걸 볼 수 있다.

  • 콜백 지옥 예시
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, '에스프레소');

각 함수에 이름을 붙여 다음에 실행될 함수를 콜백함수로 넣어주었다. 하지만 일일이 이름이 붙여야 하는 번거로움이 있다.

  • Promise
    비동기 작업을 동기적으로 처리하기 위해 Promise 객체를 사용할 수 있다
// new 연산자로 호출한 promise의 인자로 넘어가는 콜백은 바로 실행한다.
// 그 내부의 resolve 혹은 reject 함수를 호출하는 구문이 있을 경우
// resolve 혹은 reject 둘 중 하나가 실행되기 전까지는 다음(then), 오류(catch)로 넘어가지 않는다
// 따라서, 비동기 작업이 완료될 때 비로소 resolve, reject를 호출한다

new Promise(function (resolve) {
  setTimeout(function () {
    var name = "에스프레소";
    console.log(name);
    resolve(name);
  }, 500);
})
  .then(function (prevName) {
    // 이 안에서도 새로게 promise를 만든다
    return new Promise(function (resolve) {
      setTimeout(function () {
        var name = prevName + ", 에스프레소";
        console.log(name);
        resolve(name);
      }, 500);
    });
  })
  .then(function (prevName) {
    // 이 안에서도 새로게 promise를 만든다
    return new Promise(function (resolve) {
      setTimeout(function () {
        var name = prevName + ", 카페모카";
        console.log(name);
        resolve(name);
      }, 500);
    });
  })
  .then(function (prevName) {
    // 이 안에서도 새로게 promise를 만든다
    return new Promise(function (resolve) {
      setTimeout(function () {
        var name = prevName + ", 카페라떼";
        console.log(name);
        resolve(name);
      }, 500);
    });
  });

에스프레소
에스프레소, 에스프레소
에스프레소, 에스프레소, 카페모카
에스프레소, 에스프레소, 카페모카, 카페라떼
=> Promise의 인자로 넘어가는 콜백함수는 바로 실행된다 즉 function (resolve) {
setTimeout(function () {
var name = "에스프레소";
console.log(name);
resolve(name);
}, 500);
})가 즉시 실행된다. 이 콜백 함수는 resolve와 reject 두 개의 콜백 함수를 인자로 가지며 각각 비동기 작업이 성공 / 실패했을 때 호출한다. resolve가 실행되면 then 메서드가 실행되어 다음 코드를 실행한다.

  • 리팩토링
var addCoffee = function (name) {
  return function (prevName) {
    // 이 안에서도 새로게 promise를 만든다
    return new Promise(function (resolve) {
      setTimeout(function () {
        // 백틱
        var newName = prevName ? `${prevName}, ${name}` : name;
        console.log(newName);
        resolve(newName);
      }, 500);
    });
  };
};

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

에스프레소
에스프레소, 아메리카노
에스프레소, 아메리카노, 카페모카
에스프레소, 아메리카노, 카페모카, 카페라떼
=> addCoffee("에스프레소")() addCoffee 함수를 호출한다 이때 name 인자에 "에스프레소"를 전달한다. 그 결과로 prevName을 매개변수로 하는 함수를 반환한다. 하지만 호출 시 prevName 인자를 생략하였으므로 var newName = prevName ? ${prevName}, ${name} : name; 이 삼항 연산자에 의해 newName에 "에스프레소"만 할당한다. 그리고 resolve가 호출되어 newName을 반환한다. 이후 반환된 값이 then 메서드에 전달된다.

  • Generator
    함수에 *을 붙여 사용. 실행 시 Iterator 객체를 반환한다. 이 객체는 next() 메서드를 갖는다. Generator 함수는 yield 표현식에서 일시적으로 멈춘다. 이때 next 메서드로 멈춘 부분부터 다시 실행한다.
// 1 제너레이터 함수 안에서 쓸 addCoffee 함수 선언
var addCoffee = function (prevName, name) {
  setTimeout(function () {
    coffeeMaker.next(prevName ? prevName + ", " + name : name);
  }, 500);
};

// 2 제너레이터 함수 선언
// yield 키워드로 순서 제어
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(mocha);
  var latte = yield addCoffee(mocha, "카페라떼");
  console.log(latte);
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

에스프레소
에스프레소, 아메리카노
에스프레소, 아메리카노, 카페모카
에스프레소, 아메리카노, 카페모카, 카페라떼
=> coffeeMaker.next(); 제너레이터 함수 실행.
var espresso = yield addCoffee("", "에스프레소"); yield 표현식에서 일시적으로 멈추고 비동기 작업을 기다린다. (addCoffee)
addCoffee가 호출되어 500ms 이후 next 메서드를 호출하여 중단했던 부분부터 다시 코드를 실행한다.

  • async / await
    비슷한 방법으로 anync / await를 사용할 수 있다. 비동기 작업을 수행할 함수 앞에 anync, 함수 내부에서 비동기 작업이 필요한 위치마다 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));
  };

  //   promise를 반환하는 함수인 경우 await를 만나면 무조건 끝날 때까지 기다린다
  await _addCoffee("에스프레소");
  console.log(coffeeList);
  await _addCoffee("아메리카노");
  console.log(coffeeList);
  await _addCoffee("카페모카");
  console.log(coffeeList);
  await _addCoffee("카페라떼");
  console.log(coffeeList);
};
coffeeMaker();

에스프레소
에스프레소, 아메리카노
에스프레소, 아메리카노, 카페모카
에스프레소, 아메리카노, 카페모카, 카페라떼
=> coffeeMaker(); 호출. 이는 anync 함수이다.
await _addCoffee("에스프레소"); _addCoffee 호출 이때 작업이 끝날 때까지 기다린다.
coffeeList += (coffeeList ? ", " : "") + (await addCoffee(name)); 처음 할당된 coffeeList의 값은 빈문자열이므로 "" 반환. 그리고 addCoffee 호출하여 반환한 값을 붙인다.
addCoffee를 호출하여 500ms 후에 "에스프레소" 반환. 이를 coffeeList에 더한다.

profile
안녕하세요

0개의 댓글