본문 바로가기

자바스크립트

[JS] this 바인딩과 화살표 함수

자바스크립트에서 this는 다른 객체지향 프로그래밍 언어들과는 많이 다르다. 저번 주에 팀원들과 딥 다이브 스터디를 하면서, 인스턴스 객체의 메서드들은 화살표함수로 정의하는 것이 좋다고 설명을 했지만, 준비성 부족으로 인해 예시를 들고오지 못했었다... 그런데 오늘 바닐라JS 컴포넌트 구현을 연습하면서 예시를 발견했음. 이 밖에도 this를 잃어버리는 경우가 많겠지만 (1)어떤 경우에 this를 잃어버리는지, 그리고 (2)이때 this는 무엇을 가리키는지, 그리고 (3)왜 화살표 함수를 사용해야 하는지에 대해서 간단한 예시와 함께 정리해보았다.

예시) 누르면 숫자가 1씩 커지는 버튼

 

Counter

 

html body

html body에는 id가 'root'인 빈 div 태그 하나만 있습니다. 여기에 버튼엘리먼트를 추상화하여 렌더링해보겠습니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
  <script type="module" src="app.js"></script>
</html>

Counter

불러올 app.js 파일 구성은 아래와 같습니다. 코드를 자세히 보실 필요는 없습니다.

function Counter({ $target }) {
  this.state = {
    currentNumber: 0,
  };

  this.setState = function (nextState) {
    this.state = nextState;
    this.render();
  };

  this.onClickEventHandler = function () {
    console.log(this); // 여기서 this는 무엇을 가리킬까요?
    this.setState({
      currentNumber: this.state.currentNumber + 1,
    });
  };

  this.render = function () {
    $button.textContent = this.state.currentNumber;
  };

  const $container = document.querySelector($target);
  const $button = document.createElement("button");
  $button.addEventListener("click", this.onClickEventHandler.bind(this));
  $container.appendChild($button);
  this.render();
}

new Counter({ $target: "#root" });


(일부러 class가 아닌 function으로 인스턴스를 만들었습니다)

  1. new Counter로 Counter 인스턴스를 불러옵니다. 그리고 객체 형태의 파라미터에 부모노드를 설정합니다.
  2. this.state 프로퍼티를 통해 숫자를 관리합니다.
  3. this.setState 메서드를 통해 상태(thit.state)를 변경합니다.
  4. this.onClickEventHandler 메서드를 통해서 생성한 버튼에 'click' 이벤트를 붙여줍니다.
  5. this.render를 통해 버튼에 값을 렌더링 합니다.

중점적으로 봐야할 부분

정말 코드를 자세히 보실 필요는 없습니다. 몇 가지만 중점적으로 보시면 될 것 같습니다.

  1. 메서드가 화살표 함수로 정의되지 않았습니다.
  2. 생성한 button ($button)에 이벤트 핸들러를 붙일 때 this를 바인딩 하고 있습니다.

왜 이 부분을 중점적으로 봐야할까요?

아래 경우를 차례대로 살펴보시면 좋습니다. (직접 해보시면 더욱 좋습니다)

1. this.onClickEventHandler 메서드 안의 this를 확인해본다.

이 때, this는 Counter 객체를 가리키고 있습니다.

2. this를 바인딩한 부분을 제거하고 다시 this.onClickEventHandler 메서드 안의 this를 확인해본다.

$button.addEventListener("click", this.onClickEventHandler.bind(this)); // 를
$button.addEventListener("click", this.onClickEventHandler); // 로 변경해보세요.

이 때, this는 생성된 버튼을 가리킵니다. 그 바로 아래 코드에서 this.setState를 실행하고 있는데, this는 더 이상 Counter를 가리키고 있지 않으므로 에러를 출력합니다. 왜냐하면, this.onClickEventHandler는 $button에 연결된 객체이기 때문입니다. 즉 this가 바뀌었습니다. (this를 잃어버린 셈)

3. 모든 메서드를 화살표 함수로 바꾼다.

(사실 this.onClickEventHandler 메서드만 화살표 함수로 바꾸면 됩니다.)

앱은 다시 정상작동합니다. this.onClickEventHandler 메서드 안의 this는 다시 Counter를 가리키고 있기 때문입니다.

이유는...

이 개념에 대해서 깊게 포스팅을 하기에는 아직 무리가 있지만 몇 가지 사실을 알 수는 있습니다.

  1. 화살표 함수는 문맥적으로 this를 고정한다. (렉시컬 컨텍스트 환경과 관련이 있습니다)
  2. 자바스크립트의 this는 조금 다르게 동작한다.

화살표 함수는 문맥적으로 this를 고정시킨다.

우리가 의도한대로, 또 많은 객체지향언어가 그러하듯 문맥적으로 this를 고정(해당 인스턴스 객체에 고정)시키기 위해서는 화살표 함수가 매우 효율적입니다.

자바스크립트의 this는 조금 다르게 동작한다.

자바스크립트의 this는 여러가지 개념을 많이 수반하지만, 이 경우에 this는 함수를 호출한 방법에 따라 좌우된다는 것을 알 수 있습니다. 간단한 예제를 통해 조금 더 알아보겠습니다.

1. new 키워드를 통해 함수를 호출하면 함수의 이름으로 된 객체가 메모리 어딘가에 생성됩니다. 따라서 이 때 this는 이 인스턴스 객체를 가리키게 됩니다.

function Person() {
  console.log(this); // Person {}
}

const person = new Person(); // new를 통해 함수 호출(인스턴스 객체 생성자 함수 호출)


2. 이제 이 Person에 몇 가지 프로퍼티와 메서드를 추가해보겠습니다.

function Person() {
  console.log(this); // Person {}
  this._name = "yong";
  this._age = 20;

  this.getName = () => this._name;

  this.getAge = function () {
    return this._age;
  };
}

const person = new Person();

내부 프로퍼티라는 것을 나타내기 위한(컨벤션) prefix로 '_'를 붙였습니다. (private 속성 접근자로 사용할 수 있는 최근 추가된 문법 '#'은 사용하지 않겠습니다)

3. getName과 getAge를 호출하면 당연히(?) 의도한대로 값을 반환합니다.

console.log(person.getName()) // 'yong'
console.log(person.getAge()) // 20


4. 자바스크립트의 함수는 일급이므로 이제 여기서 person의 name과 age를 반환하는 getName과 getAge를 다른 변수에 담아보겠습니다. 그리고 값을 출력해보면...

const personName = person.getName;
const personAge = person.getAge;

console.log(personName()); // 'yong'
console.log(personAge()); // undefined

personName과 달리 personAge는 undefined를 출력했습니다.

❓ 이유는 무엇일까요?

  1. personAge 함수는 호출 대상이 누구인지 모릅니다. 그래서 undefined를 반환했습니다.
  2. personName은 person.getName 입니다. 그리고 person.getName은 화살표 함수로 정의됐기 때문에 this를 잃어버리지 않고 'yong'을 출력했습니다.

this가 함수를 호출한 방식에 따라 좌우된다는 것은 이런 것이고, 화살표 함수는 문맥적으로 this를 고정시켰기 때문에 this를 잃어버리지 않았습니다.

가장 처음 예시로 보았던 '누르면 숫자가 1씩 커지는 버튼'의 예시도 이와 같은 이유였다는 것을 알 수 있습니다.

this 바인딩

this를 바인딩, 즉 의도한대로 this를 잃어버리지 않기 위해서는 몇 가지 방법이 있습니다. 앞서 bind(this)화살표 함수로 메서드 정의 외에도,

call()

console.log(personAge.call(person)); // 20

call 함수를 이용해서 어떤 객체를 가리키고 있는지 지정할 수 있습니다.

apply()

console.log(personAge.apply(person)); // 20

apply 함수도 유사합니다.

apply와 call은 사용법이 약간 다를뿐 거의 동일하게 동작합니다.


💡 정리

  1. 자바스크립트의 this는 함수 호출 방식에 따라 달라진다.
  2. 화살표 함수를 이용해서 this를 문맥적(렉시컬 컨텍스트) 으로 고정시킨다.
  3. bind(), call(), apply() 로 바깥에서 객체를 직접 가리키게 한다.

🔗 참고

  • 패스트캠퍼스 <김민태의 자바스크립트 & 타입스크립트 에센셜>
  • this - JavaScript | MDN
  • 프로그래머스 데브코스

추가

화살표 함수를 쓰는 것이 더 좋아보이지만, 무조건은 아니다. 실행 컨텍스트 환경의 this를 이용해서 고급 기법을 구사할 수 있다고 한다(는데 아직 잘 모르겠다).


데브코스에서 컴포넌트 렌더링을 연습하다가 this와 화살표 함수에 꽂혀 조금(?) 알아보았다. 조금만 알아보았는데 컨텍스트가 등장했다. 컨텍스트는 정말 계속 나온다. 빠르면 이번주, 늦으면 다음주 내로 부셔본다... 🔥🔥🔥