본문 바로가기

Web + APP/React

이벤트 리스너 캐시

반응형
SMALL

제가 최근 과제를 진행하며, render 함수에 이벤트 리스너를 추가하는 코드를 많이 짰는데, 이러한 코드는 성능을 떨어뜨린다는 피드백을 많이 받았습니다.

 

도대체 왜 그런 것일까가 궁금해서 찾아보았고 아래의 글을 찾게 됐습니다.

 

본 글은 아래의 글을 정리하는 글이 될 것 같네요.

 

ui.toast.com/weekly-pick/ko_20180911

 

이벤트 리스너 캐시를 이용한 React 성능 향상 | TOAST UI :: Make Your Web Delicious!

이벤트 리스너 캐시를 이용한 React 성능 향상 저자 : Charles Stover / 웹사이트 : charlesstover.com / LinkedIn : https://www.linkedin.com/in/charles-stover / Twitter : https://twitter.com/CharlesStover 역자 : 박정환(FE개발랩) 원

ui.toast.com


JS 객체와 함수는 참조형입니다. 그렇기에 완전 같은 함수를 마들어도 두 함수는 같지 않습니다.

const functionOne = function() { alert('Hello World'); };
const functionTwo = function() { alert('Hello World'); };
functionOne === functionTwo; // false

이미 만들어진 함수나 객체를 변수에 할당한 결과는 다르겠죠?

const functionThree = function() { alert('Hello World'); };
const functionFour = functionThree;
functionThree === functionFour; // true

const obj1 = {};
const obj2 = {};
const obj3 = obj1;

obj1 === obj2; // false
obj1 === obj3; // true

포인터란 개념을 생각해보죠. obj1 = {}을ㄹ 하면 한 덩어리의 바이트를 RAM에 생성하면서 사용합니다. obj2 = {} 를 또하면 obj1와는 다른 공간에 한 덩어리의 바이트를 RAM에 만듭니다. 둘의 메모리 주소는 다릅니다. obj3은 obj1의 메모리 주소를 넣었기에 같다고 인지되는 것입니다.

 

obj1을 수정하면 obj3도 수정되고 obj3를 수정하면 obj1도 수정되겠죠?

 

그래서 object를 수정하고 싶으면 Object.assign() 메소드를 사용해야하는 이유가 여기 있습니다.


이제 React 얘기를 좀 해볼까요?

 

React의 경우 성능을 끌어올리기 위해서 props와 state가 변경되지 않으면 render 출력 및 변경을 하지 않습니다. 왜냐면 변경된 것이 없는데, 굳이 render를 또 호출할 이유가 없으니까요.

 

React는 JS와 같은 방식으로 props와 state가 같은지 판단하는데, == 연산자로 확인합니다. 객체가 같은지 깊게 비교하지 않고, 메모리 주소만 비교하는 것입니다.

 

그렇기에, props = { x: 1 }를 { x: 1 }로 바꾸면, 다시 render가 그려지게 됩니다. 주소가 다르니까요. 위의 예에서 봤듯이 obj1 => obj3의 경우엔 바뀌지 않겠죠. 주소가 같으니까요.

 

그렇기 때문에, 아래의 코드는 문제가됩니다.

class SomeComponent extends React.PureComponent {

  get instructions() {
    if (this.props.do) {
      return 'Click the button: ';
    }
    return 'Do NOT click the button: ';
  }

  render() {
    return (
      <div>
        {this.instructions}
        <Button onClick={() => alert('!')} />
      </div>
    );
  }
}

Button 컴포넌트가 있고 그걸 클릭했을 때, 경고창이 뜨는데, SomeComponent의 경우 do={true}, do={false} prop으로 제어됩니다.

 

SomeComponent가 true / false로 토글되면 버튼 컴포넌트가 다시 그려지게 됩니다. onClick 핸들러 함수 내용은 같더라도 새로운 메모리에 함수가 생성되기 때문이고, 메모리 주소 참조가 Button 컴포넌트로 넘어가고 있기 때문에 변경이 없더라도 Button은 다시 그려지는 것이죠.


해결책은 없을까요?

 

함수가 해당 컴포넌트에 의존하지 않는다면, 컴포넌트 외부에 함수를 정의하면 됩니다.

const createAlertBox = () => alert('!');

class SomeComponent extends React.PureComponent {

  get instructions() {
    if (this.props.do) {
      return 'Click the button: ';
    }
    return 'Do NOT click the button: ';
  }

  render() {
    return (
      <div>
        {this.instructions}
        <Button onClick={createAlertBox} />
      </div>
    );
  }
}

이전 예제와는 반대로 createAlertBox는 매번 같은 메모리에 있는 함수를 참조하기에, 다시 그려지지 않습니다.

 

사실 Button의 경우엔 그리는데 오래 걸리지 않지만, 이러한게 많아지는 경우 (for 문 내부에서 해당 컴포넌트를 그리는 경우) 앱이 느려질 수있죠. 그렇기에 함수들은 컴포넌트 render 함수에서 정의하지 않는 것을 추천합니다.

 

해당 내용은 ggodong.tistory.com/255 (if kakao 2020 - FE 개발 서바이벌 키트)에서도 마지막에 나온 내용입니다.

 

물론, 함수가 컴포넌트에 의존하면, 안에다가 메소드를 만들어서 넘겨줘도 됩니다. Some Component의 인스턴스는 각기 다른 경고창을 가지게 될 것이고, 자신의 createAlertBox 메소드를 넘기기 때문에, SomeComponent가 그려지더라도, Button 내의 createAlertBox의 메모리 주소가 변하지 않기 때문에 Button 컴포넌트는 다시 그려지지 않게 됩니다.

class SomeComponent extends React.PureComponent {

  createAlertBox = () => {
    alert(this.props.message);
  };

  get instructions() {
    if (this.props.do) {
      return 'Click the button: ';
    }
    return 'Do NOT click the button: ';
  }

  render() {
    return (
      <div>
        {this.instructions}
        <Button onClick={this.createAlertBox} />
      </div>
    );
  }
}

만약, 함수가 동적으로 변경된다면 어떻게 될까요?

class SomeComponent extends React.PureComponent {
  render() {
    return (
      <ul>
        {this.props.list.map(listItem =>
          <li key={listItem.text}>
            <Button onClick={() => alert(listItem.text)} />
          </li>
        )}
      </ul>
    );
  }
}

li 태그 하나하나마다 동적 이벤트 리스너가 있는 상황입니다. 배열을 매핑하는 경우죠. 이런 경우엔 메모이제이션, 캐싱을 사용하면 됩니다.

 

각 고유한 값마다 함수를 생성하고 캐싱하며, 그 참조 값에 함수가 있다면 참조를 그대로 사용하면 됩니다.

class SomeComponent extends React.PureComponent {

  // SomeComponent의 각 인스턴스는 인스턴스마다 고유한 클릭 핸들러들을 캐싱한다.
  clickHandlers = {};

  // 고유 식별자에 따른 클릭 핸들러를 반환한다. 없다면 생성하고 반환한다.
  getClickHandler(key) {

    // 고유 식별자에 대한 이벤트 핸들러를 만들지 않았다면, 새로 생성한다.
    if (!Object.prototype.hasOwnProperty.call(this.clickHandlers, key)) {
      this.clickHandlers[key] = () => alert(key);
    }
    return this.clickHandlers[key];
  }

  render() {
    return (
      <ul>
        {this.props.list.map(listItem =>
          <li key={listItem.text}>
            <Button onClick={this.getClickHandler(listItem.text)} />
          </li>
        )}
      </ul>
    );
  }
}

배열에 있는 각 아이템은 getClickHandler 메서드를 통해 이벤트 리스너를 받는데, 고윳값에 해당하는 값이 있는지 확인 후 있으면 그걸 받고, 없으면 만드는 식으로 저장합니다.

 

그 결과 SomeComponent가 다시 그려질 때 Button도 다시 그려지는 일은 일어나지 않죠.

 

참고로 식별자로 배열의 인덱스를 사용하는 것은 위험합니다. 배열의 인덱스를 식별자로 사용하면, 배열의 변화에 따라서 개발자가 예상치 못 한 문제를 만나기 때문입니다.

 

['a', 'b']라는 배열을 ['b']라고 바꾼다면 캐싱한 이벤트 리스너가 listeners[0] = () => alert('b')가 되므로 0번째 아이템 클릭 시 'b'라는 경고창이 뜨는데, 해당 내용은 React 공식문서에서도, 배열의 인덱스를 식별자로 사용하지 말라는 이유입니다.

배열의 인덱스를 식별자로 쓰지 말자


이상 이벤트 리스너 캐시였습니다. ^_^

반응형
LIST

'Web + APP > React' 카테고리의 다른 글

Flux 패턴  (0) 2021.03.19
React 소개 with 이벤트 위임  (4) 2021.03.16
React Form과 bind 개념  (4) 2020.06.10
React Project에 Tailwind 적용하기  (1) 2020.06.07