this 바인딩 (feat. NEXT STEP 자동차 경주 미션)

이현정·2022년 7월 31일
0
post-thumbnail

들어가며...

class RacingModel {
  constructor() {
    this.carNames = [];
    this.count = 0;
    this.arrows = [];
    this.winners = [];
  }

  #getRandomInt(min, max) { ...
  };

  #isAbleToMoveFoward() { ...
  };

  setCarNames($carNames) { ...
  };

  setCount($count) { ...
  };

  displayCars() { ...
  };

  startRacingGame($count) { ...
  };

  getRacingResult() { ...
  };

  showGameResult() { ...
  };

NEXT STEP 의 3번째 미션 자동차 경주 게임을 진행하면서 상태관리를 위해 모델을 만들어 보라는 리뷰를 받았다.

처음 클래스를 이용하여 만들어 보았다.
대강 위와 같은 구성이었는데 이중 가장 핵심적인 기능을 하는 startRacingGame 매서드는 아래와 같았다. 오류가 발생한 지점이기도 하다.

startRacingGame($count) {
    let cnt = 1;

    const timeoutId = setInterval(() => {
      displayTemplateForward(
        $$(SELECTORS.CAR_DIV_NAME),
        this.#isAbleToMoveFoward
      );

      if (cnt++ === $count) {
        clearInterval(timeoutId);
        this.getRacingResult();
        removeSpinners($$(SELECTORS.CAR_DIV_SPINNER));
        this.showGameResult();
        setTimeout(() => {
          alert("🎇🎇🎇🎇 축하합니다! 🎇🎇🎇🎇");
        }, 2000);
      }
    }, 1000);
  }

위 startRacingGame 매서드 코드를 요약하면 다음과 같다:

  • setInterval: 시간차(1초) 간격으로 displayTemplateForward 함수를 실행하고, 실행시마다 cnt 를 1씩 증가시켜라.
  • if: cnt값이 사용자에게 입력 받은 실행횟수($count)와 일치되면(=함수가 사용자가 원하는 만큼 실행되었다면)
  • clearInterval: setInterval 에 등록한 함수를 중지하고
  • 그이후 함수들을 실행해라.

+) displayTemplateForward 함수는 다른 모듈에서 import 해온 함수로, 두번째 인자로 받는 this.#isAbleToMoveForward 함수가 true를 반환할 때만 실행되는(템플릿을 DOM에 삽입) 조건식을 가지고 있다.

오류: this is undefined

앱을 실행해보니 displayTemplateForward가 실행되지 않았다.
debugger로 찍어보니 this 가 undefined 라고 뜬다.

#getRandomInt (min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
  };

#isAbleToMoveFoward () {
debugger;
const randomNum = this.#getRandomInt(1, 10);
return randomNum > 4;
};

왜지..? 매서드의 this 는 객체를 가리키는 게 아니였나...?

결론부터 말하자면,
인자로 받는 this.#isAbleToMoveForward 와 그안에서 호출되는 함수인 this.#getRandomeInt 가 문제였다.
두 함수 모두 this 가 객체 Racing Model 이 아닌 undefined 를 말하고 있었다.

this 바인딩에 대한 이해가 부족해 발생한 실수였다.
this 바인딩에 대해 알아보자.

this 바인딩

함수 호출 방식

this 바인딩(this에 연결될 값)은 함수 호출 방식, 즉 함수가 어떻게 호출되었는지에 따라 동적으로 결정된다.
함수를 호출하는 방식은 아래와 같다.

  • 1. 일반 함수 호출
    • ex) foo()
  • 2. 메서드 호출
    • ex) obj.foo()
  • 3. 생성자 함수 호출
    • ex) new foo()
  • 4. Function.prototype.apply/call/bind 메서드에 의한 간접 호출
    • ex) foo.call(obj);
      foo.appply(obj);
      foo.bind(obj)();

위 함수 호출 방식에 따른 this 바인딩은 아래와 같다.

const foo = function () {
  console.dir(this);
};

// 1. 일반 함수 호출
foo(); //=> window. this는 전역객체를 가리킨다.

// 2. 메서드 호출
const obj = { foo };
obj.foo(); //=> obj. this는 메서드를 호출한 객체 obj 를 가리킨다.

// 3. 생성자 함수 호출
new foo (); //=> foo {}. this는 생성자 함수가 생성한 인스턴스를 가리킨다.

// 4. Function.prototype.apply/call/bind 메서드에 의한 간접 호출
const bar = { name:"bar" }
foo.call(bar); //=> bar. 
foo.apply(bar); //=> bar. 
foo.bind(bar)(); //=> bar. this는 인수에 의해 결정된다.

➡️ 나의 경우는 1,2번 케이스만 알면 되었다.

this 바인딩: 일반 함수 호출의 경우

this 바인딩: 일반 함수 호출의 경우

  • 기본적으로 this 에는 전역 객체가 바인딩 된다.

  • 중첩함수든 콜백함수든 객체내에 선언된 메서드든
    어떤 함수든지 일반 함수 호출로 호출되었다면 전역 객체가 바인딩된다.

  • 단, strict 모드에서 일반 함수 호출의 경우
    this 에는 undefined 가 바인딩 된다.

this 바인딩: 메서드 호출의 경우

  • this메서드를 호출한 객체를 가리킨다.

문제 파악

다시 정리하면

메서드 내부의 중첩 함수나 콜백 함수의 this 바인딩 => 전역 객체
메서드의 this 바인딩 => 메서드 호출 객체

메서드 내부의 중첩 함수나 콜백 함수의 this 바인딩 ≠ 메서드의 this 바인딩

이다.

내 경우의 경우이 부분이 문제였다.
앞부분의 오류가 난 코드 부분을 다시 가져와 보면,

// RaicingModel.js 
class RaicingModel {
  constructor {...}
  .
  .
  .
  startRacingGame($count) {
      let cnt = 1;

      const timeoutId = setInterval(() => {
        displayTemplateForward(
          $$(SELECTORS.CAR_DIV_NAME),
          this.#isAbleToMoveFoward
        );
   .
   .
   .

RacingModel 객체 안의 startRacingGame 메서드가 제대로 실행되지 않았다.
그 이유는 displayTemplateForward 함수가 받는 두 번째 인자 this.#isAbleToMoveForward 의 this가 undefined 로 떴기 때문이다.

(기억하자. 일반 함수 호출로 실행된 함수 -이 경우 displayTemplateForward() - 의 this전역객체(window)다. strict mode 로 실행되면 undefined.)

문제가 된 this.#isAbleToMoveForward 메서드의 코드를 봐보자.

// RaicingModel.js 
class RaicingModel {
  constructor {...}
  .
  .
  #getRandomInt (min, max) {
      min = Math.ceil(min);
      max = Math.floor(max);
      return Math.floor(Math.random() * (max - min)) + min;
    };

  #isAbleToMoveFoward () {
  debugger;
  const randomNum = this.#getRandomInt(1, 10);
  return randomNum > 4;

};

➡️ 메서드 호출 이다. #isAbleToMoveForward 메서드 역시 안에 this 를 참조하고 있다. 여기서 this 는 RacingModel 이다.

정리하면,

displayTemplate()의 this => window
#isAbleToMoveForward의 this => Racing Model

displayTemplate()의 this (window) ≠ #isAbleToMoveForward의 this (Racing Model)

로 둘이 가리키는 this 가 같지 않은데 displayTmeplate()의 this 가 Racing Model 을 가리킬거라 생각하고 코드를 짜서 생긴 문제였다.

문제 해결 방법 모색

그렇다면 메서드 내부의 중첩 함수나 콜백 함수의 this 바인딩(=전역객체)을
메서드의 this 바인딩(=메서드 호출 객체)과 일치시키는 방법
은 없을까?

3가지 방법이 있다고 한다.

    1. 메서드 내 this 바인딩(호출 객체)를 변수에 할당에 저장해두고 그걸 일반 함수 호출 때 쓰는 방법
    1. bind 메서드를 통해 this 를 명시적으로 바인딩 해주는 방법
    1. 화살표 함수를 통해 this 를 바인딩 해주는 방법

이해를 돕기 위한 각 방법의 케이스의 예시는 아래와 같다.

// 1번: 메서드 내 this 바인딩(호출 객체)를 변수에 할당에 저장해두고 그걸 일반 함수 호출 때 쓰는 방법
var value =1;

const obj = {
  value: 100;
  foo () {
    const that = this; // this 바인딩(=obj)를 변수 that에 할당
    
    setTimeOut(function () {
      console.log(that.value); //=> 100. this 대신 that 할당
    }, 100);
  }
};

obj.foo()
// 2번:bind 메서드를 통해 this 를 명시적으로 바인딩 해주는 방법
var value =1;

const obj = {
  value: 100;
  foo () {
    setTimeOut(function () {
      console.log(this.value);	//=> 100 
    }.bind(this), 100);		// this 를 명시적으로 할당.(여기서 this 는 foo 의 this값(=obj))
  }
};

obj.foo()
// 3번:화살표 함수를 통해 this 를 바인딩 해주는 방법
var value =1;

const obj = {
  value: 100;
  foo () {
    // 화살표 함수로 호출.
    setTimeOut(function=()=>{   // (화살표 함수 내부의 this(=obj) 는 상위 스코프의 this 를 가리킨다)
      console.log(this.value);	//=> 100 
    }, 100);		
  }
};

obj.foo()

➡️ 나는 가장 3번으로 선택해 해결했다.

문제 해결

// 3번:
#getRandomInt = (min, max) => {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
  };

#isAbleToMoveFoward = () => {
debugger;
const randomNum = this.#getRandomInt(1, 10);
return randomNum > 4;
};

마무리 하며...

요약

  • this 바인딩(this 가 가리키는 값)은 함수의 호출 방식에 따라 결정된다.
  • 함수 호출 방식은 총 4가지가 있으며, 우선 이 중 두 가지 방식(:일반 함수 호출, 매서드 호출)의 경우 this binding 을 살펴 보았다.
  • 일반 함수 호출 시 this 는 전역 객체 또는 undefined(strict mode)이다.
    매서드 호출 시 this 는 바로 호출 객체를 나타낸다.
  • 객체 내에서 메서드(this = obj) 안에 일반 함수 호출(this = window)을 했을 때 생기는 this 불일치 문제를 해결하기 위한 3가지 방법에 대해 살펴보았다.
  • 그 중 마지막 방법인 화살표 함수 방법을 이용해 발생했던 문제를 해결했다.

TODO

  • 다른 2가지 호출 방식에 따른 this 바인딩도 차차 익혀보자.
  • 객체 내에서 메서드(this = obj) 안에 일반 함수 호출(this = window)을 했을 때 생기는 this 불일치 문제를 해결하기 위한 3가지 방법 중 3가지 방법에 대해 살펴보았다.
    => 리팩토링 때 나머지 두 방법을 적용해서 해보면서 세 방법의 차이에 대해 공부해 보면 좋을 듯 하다.

0개의 댓글