Class에서 arrow function을 사용하지 말아야하는 이유

심재철
6 min readJun 30, 2020

--

이 글은 독자가 Promise에 대한 이해가 있다고 가정하고 진행합니다.

class Component {
handleClick = () => {};
handleClick();
}

클래스에서 메소드를 선언할때

arrow function을 사용하는 첫번쨰 방법(@babel/plugin-proposal-class-properties로 트랜스파일링 해야함)과

그냥 function을 사용하는 두번째 방법이 있습니다.

function을 사용하는 경우 아래처럼 굉장히 불편한 상황을 겪게 됩니다.

class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
// 2. this바인딩을 매번 해줘야 해서 귀찮다..
}

// 1. function으로 선언하는 경우
handleClick() {
}
}

그래서 function 대신 arrow function을 사용할 경우 자동으로 this바인딩되어 편리합니다.

class Component {
handleClick = () => {
console.log(this); // 자동으로 바인딩 되어 Component 인스턴스를 가리킨다.
};
}

arrow function을 사용하지 말아야 하는 이유

class Component {
static static = 0;
arrow = () => {};
normal(){}
}

이런 클래스가 있을때 바벨로 es7 트랜스파일링을 해보면

"use strict";function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }class Component {
constructor() {
_defineProperty(this, "arrow", () => {});
}
normal() {}}_defineProperty(Component, "static", 0);

이렇게 변합니다. 여기서 defineProperty는 그냥 할당문이라서 간편하게 표시하겠습니다.

class Component {
constructor() {
this.arrow = () => {};
}
normal() {}
}
Component.static = 0;

간단하게 표시해봤습니다.

주목해야할 부분은 class properties로 할당했던 arrow function이 constructor() 내부로 이동했다는 점입니다. 이것 때문에 많은 이슈가 생깁니다.

이슈 1. method override 문제

class Parent {
getName = () => {
console.log('my name is 심재철');
}
}
class Child extends Parent {
getName() {
console.log('my name is 자식');
}
}
new Child().getName();

Parent에서 getName을 arrow function으로 정의했고,

Child에서 getName을 normal function으로 정의했습니다.

그리고나서, Child의 getName() 실행하면 어떻게 될까요?

ChildgetName이니까 ‘my name is 자식’이 출력될거라고 기대해봅니다.

근데,

my name is 심재철

이 출력됬습니다. 왜 그럴까요?

Child 클래스가 인스턴스화 될때 내부적으로 부모(Parent)의 생성자를 호출해서 Child의 인스턴스 변수들을 초기화 합니다. 이때 부모의 constructor내부의 this는 Child 인스턴스를 가리킵니다. 부모의 생성자를 자식이 빌려다가 사용해서 본인의 인스턴스 변수를 초기화하는거죠.

위 코드는 바벨로 트랜스파일링하면 이렇게 변합니다.

class Parent {
constructor() {
// 2. 여기서의 this는 Child의 instance를 가리킵니다.
this.getName = () => {
console.log('my name is 심재철');
}
}
}class Child extends Parent {
getName() {
console.log('my name is 자식');
}
}
new Child().getName(); // 1. Child클래스의 instance가 생성될때

Child에서 선언했던 getName 함수가 child가 인스턴스화 되면서 부모 생성자 내부에서 override되어 버렸기 떄문입니다.

원래 우리가 기대한 정상적인 동작은, child의 getName이 있으면 실행하고 없으면 프로토타입 체이닝을 통해 getName이 있을떄까지 체인을 거슬러 올라가야 합니다.

근데, 부모에서 arrow function으로 함수를 선언한 바람에 Child의 getName은 영영 쓸 수 없는 상태가 되어 버렸습니다.

이슈2. Prototype 문제 (Performance 저하)

문제는 여기서 끝나지 않습니다.

arrow function은 constructor 내부로 이동되기 때문에 Prototype을 통해 공유되어야 할 메소드가 각 인스턴스 내부로 이동하게 됩니다. 이게 무슨 말이냐면, A라는 클래스로 100개의 인스턴스를 만들면 그 100개의 인스턴스 내부에 서로 다른 각각 100개의 함수가 중복으로 만들어진다는 말입니다. 메모리도 많이 먹고 퍼포먼스에도 꽤 영향이 크다고 합니다.

normal function으로 메소드를 선언했으면 같은 메소드가 여러번 호출되더라도 모든 인스턴스들이 단 하나의 프로토타입 메소드를 공유하기 때문에 자바스크립트 V8엔진이 성능을 최적화 해줄 수 있습니다.

이슈3. 상속 문제

arrow function을 사용하면 클래스 상속이 정상적으로 동작하지 않습니다.

arrow function으로 선언한 함수는 인스턴스 함수가 되기 때문에 클래스에 속하지 않습니다.

예를들어 A라는 클래스가 있고 B클래스가 A를 상속받는 경우 A클래스의 모든 메소드들이 B의 인스턴스에 들어있어야합니다.

근데, A에 a라는 arrow function이 있는 경우 인스턴스 메소드가 되기 때문에 B가 A를 상속받는다고 하더라도 B에 a라는 함수는 들어있지 않게 됩니다. 오로지 A의 인스턴스에만 a라는 함수가 들어있게 되는 현상이 발생하죠.

결론

클래스의 메소드를 선언할때는 normal function을 사용합시다!

arrow function을 사용하는 경우 디버깅하기 힘든 버그가 발생할 수 있습니다. (프로토타입 체인을 뒤져봐야 할 수 도 있어서..)

arrow function을 사용하는 경우

1. 메소드가 상속되지 않는다.

2. 메소드가 자식 메소드를 override 해버린다.

3. 퍼포먼스 이슈가 있다.

--

--