[번역] 클래스 프로퍼티로 화살표 함수 사용하는 것은 생각만큼 좋지않다

리액트에서 클래스 컴포넌트를 사용하다가 arrow function과 메서드 차이가 궁금해져서 찾아본 포스팅이다.

Class Properties Proposal덕분에 코드가 간단해졌는데, 특히 내부 statepropTypesdefaultProps를 static으로 작성할 때 코드 작성이 간단해졌다.

!img

게다가 클래스 프로퍼티 이니셜라이저는 생성자 함수에서 bind 함수 호출하는 것 대신에, React에서 바인딩을 처리하기 위해 지난 6개월 동안 더 유행된 것 같다.

class ComponentA extends Component {
  handleClick = () => {}

  render() {}
}

클래스 프로퍼티에서 화살표 함수 사용하기

클래스 프로퍼티에서 화살표 함수는 유용하다. 왜냐하면 autobind되고 생성자 함수에서 다음 코드를 작성할 필요가 없다. this.handleClick = this.handleClcik.bind(this)

하지만 정말로 클래스 프로퍼티에 화살표 함수를 작성해야만 할까?

먼저, 클래스 프로퍼티 아래에 뭐가 있는지 살펴보자.

What Class Properties look like once transpiled to ES2017

static/instance property, arrow function, usual function 을 가진 간단한 클래스를 코드로 작성하자.

class A {
  static color = "red";
  counter = 0;

  handleClick = () => {
    this.counter++;
  }

  handleLongClick() {
    this.counter++;
  }

Babel REPL로 가서 es2017과 stage-2 프리셋으로 트랜스파일된 클래스를 살펴보자.

// 약식 version
class A {
  constructor() {
    this.counter = 0

    this.handleClick = () => {
      this.counter++
    }
  }

  handleLongClick() {
    this.counter++
  }
}
A.color = 'red'

인스턴스 프로퍼티는 생성자 함수 내부로 이동하고 static은 class 선언문 뒤로 이동되었다.

개인적으로, static keyword 추가를 좋아한다. 왜냐하면 static properties와 함께 클래스를 직접 export 할 수 있기 때문이다.

인스턴스 프로퍼티도 좋아하는데, 왜냐하면 코드에서 생성자 함수가 커지지 않기 때문이다.

프로퍼티에서 화살표 함수의 경우, handleClick이 생성자 함수로 이동되었다. (instance property 처럼)

usual function method인 handleLongClick은 변한게 없다.

프로퍼티 이니셜라이저는 유용하지만 클래스 프로퍼티로 arrow function 사용시, binding 하는 방식이 hackish하다.

Mockability

class method를 모킹하거나 추적?(spy)하기를 원할 때, 프로토타입을 사용하는 것이다. (왜냐하면 가장 쉬운 방법은 프로토타입 체이닝을 통해 object prototype 객체의 모든 변경사항을 볼 수 있으므로)

class A {
  static color = 'red'
  counter = 0

  handleClick = () => {
    this.counter++
  }

  handleLongClick() {
    this.counter++
  }
}

A.prototype.handleLongCLick은 정의되었다.

A.prototype.handleClick은 undefined다.

class property에서 arrow function을 사용했기 때문에, handleClick은 생성자 이니셜라이징 될 때만 정의되고 prototype 내부에 없다.

그래서, 이니셜라이징된 오브젝트에서 함수를 모킹 해보면 프로토타입 체이닝을 통해 볼 수 없다면

Inheritance

base class A를 정의하자.

class A {
  handleClick = () => {
    console.log('A.handleClick')
  }

  handleLongClick() {
    console.log('A.handleLongClick')
  }
}

console.log(A.prototype)
// {constructor: ƒ, handleLongClick: ƒ}

new A().handleClick()
// A.handleClick

new A().handleLongClick()
// A.handleLongClick

만약 class B가 A를 상속받으면, handleClick은 프로토타입에 없고 super.handleClick을 호출 할 수 없다.

class B extends A {
  handleClick = () => {
    super.handleClick()

    console.log('B.handleClick')
  }

  handleLongClick() {
    super.handleLongClick()

    console.log('B.handleLongClick')
  }
}

console.log(B.prototype)
// A {constructor: ƒ, handleLongClick: ƒ}

console.log(B.prototype.__proto__)
// {constructor: ƒ, handleLongClick: ƒ}

new B().handleClick()
// Uncaught TypeError: (intermediate value).handleClick is not a function

new B().handleLongClick()
// A.handleLongClick
// B.handleLongClick

만약 A클래스의 상속을 받은 C클래스가 handleClick을 arrow function 대신 usual function으로 구현한다면, handleClicksuper.handleClick()만 실행한다. 이상하지 않니..?

부모 클래스의 생성자 내부에서 handleCLick의 이니셜라이징이 그것을 오버라이드 하기 때문이다.

class C extends A {
  handleClick() {
    super.handleClick()

    console.log('C.handleClick')
  }
}

console.log(C.prototype)
// A {constructor: ƒ, handleClick: ƒ}

console.log(C.prototype.__proto__)
// {constructor: ƒ, handleLongClick: ƒ}

new C().handleClick()
// A.handleClick

Performance