본문 바로가기
FrontEnd/React.js

[React.js] 제로초 웹게임 - 3. 숫자야구 (shouldComponentUpdate, React.memo)

by Chaedie 2022. 10. 24.
728x90

강의를 통해 배운 것

리액트 데브 툴을 활용하면 어디서 ReRendering이 발생하는지 확인 할 수 있다. 이를 통해 성능 최적화가 가능해지고, 이 때 불필요한 리렌더링을 막는 방법이 "shouldComponentUpdate, PureComponent, React.memo"이다.

이전 까진 리렌더링 최적화에 대해선 생각해본적이 없었고, 또한 함수 컴포넌트만 사용하다 보니 setState에 같은 값이 들어가면 (참조형 변수 포함) 리렌더링이 되지 않는다는 것 까지만 알고 있었다. 하지만 class형 컴포넌트를 사용하는 경우 shouldComponentUpdate와 PureComponent를 알아야 하고, 함수 컴포넌트를 사용한다 해도 자식 컴포넌트에서 props가 변경되지 않을 때 부모 컴포넌트의 리렌더링에 의해 억울하게(?) 리렌더링 되는걸 막기 위해 React.memo를 사용하면 된다는것을 배웠다.

이런 내용을 모르더라도 작은 프로젝트는 충분히 문제없이 돌아가기 때문에 혼자 공부할 땐 찾아 나서기가 쉽지 않을 수 있는데, 좋은 강의 덕에 차근차근 배울 수 있어서 좋았다.

공식문서를 언젠가 한번은 정독해야 할텐대, 우선 좋은 강의를 통해 강제로 대부분의 기능을 알게 되는 중이라 공식문서 읽기는 나중으로 미뤄둬야 할 것 같다. 제로초님 말씀 처럼 출퇴근을 하게 되면 오며 가며 공식 문서를 읽어봐야겠다.


3. 숫자야구


import vs require

require는 node의 모듈시스템이고, import는 es의 모듈 시스템이다. 둘이 엄밀하게 따지면 다르지만 대강 호환이 되고, 특히 웹팩은 node로 만든거라서 import를 모르지만 babel이 require로 해석해주기 때문에 에러없이 진행이 되는 것이다.


숫자야구 만들기

Untitled

숫자 야구 컴포넌트를 다 만들고, 실행까지 해보았다. 문제 없이 다 진행이 되었다. 하지만 dist 폴더가 생기지 않았고, app.js 또한 없다. 어디로 간걸까?

좀 찾아봤는데 해답이 안나와서 일단은 넘어가야겠다. 웹팩 때문에 진도가 너무 안나간다. ㅎㅎ


궁금한점 - return 뒤에 함수 호출문을 넣어도 되나?

if (answer.join('') === input) {
    setResult(`홈런! ${tries.length + 1}번만에 맞췄습니다!`);
    return reset();
  }
if (answer.join('') === input) {
    setResult(`홈런! ${tries.length + 1}번만에 맞췄습니다!`);
    reset();
    return;
  }

위 두 코드의 실행 결과는 똑같다. return문에 reset()이 들어있더라도 reset 함수가 호출이 되면서 reset()의 return 값인 undefined가 return 되기 때문이다.

그럼 위 처럼 사용하는게 좋은 패턴일까?

결론은 안났지만, 코드 줄수가 한줄 짧아진다 하더라도 위 패턴 보다는 아래 패턴이 좀 더 직관적인 것 같다.

  • 아래 패턴 : reset()을 실행하고 나서 return 으로 함수를 빠져나간다.
  • 위 패턴 : return으로 함수를 빠져나가지만 reset()을 호출하면서 빠져나가니까 reset의 return 도 확인해야되고, … 순서도 오른쪽 부터 보고 왼쪽을 봐야하니까 복잡하다.

🔥 개인적으로 내린 결론은 아무리 줄수를 한 줄 줄일수 있더라도 아래처럼 reset() ⇒ return 되도록 하는게 직관적이고 오해의 소지가 적다.


useState의 initial Value에 함수를 넣을 때

// 1) 이니셜 밸류에 함수 호출
const [answer, setAnswer] = useState(getNumbers());

// 2) 이니셜 밸류에 함수
const [answer, setAnswer] = useState(getNumbers);

함수 컴포넌트에서는 rerendring 될 때마다 해당 컴포넌트가 호출된다. 그래서 1) 처럼 코드를 작성하면 useState의 이니셜밸류가 바뀌진 않더라도 getNumbers()함수가 계속 실행이된다.

이를 해결 하기 위해 이니셜 밸류에 함수의 리턴값을 넣어주고 싶을 때는 2)처럼 함수만 넣어주면 이니셜 밸류를 넣을 때 해당 함수를 1번 호출해서 return값을 넣어 주고 이 후 리렌더링 될 때는 getNumbers를 호출하지 않는다.


shouldComponentUpdate()

클래스 컴포넌트에서는 this.setState()를 호출하기만 해도 (값이 그대로라도) 리렌더링이 되는 이슈가 있다. 이 때 shouldComponentUpdate()를 통해 리렌더링 여부를 true false로 return 해줄 수 있다.

이전에 한번 확인했던 내용인데, 다시 나와서 복습차원에서 좋았다. 함수 컴포넌트에서는 이런 현상이 없다.

import React, { Component, useState } from 'react';

class Test extends Component {
  state = {
    counter: 0,
  };

  shouldComponentUpdate(nextProps, nextState, nextContext) {
    if (this.state.counter !== nextState.counter) {
      return true;
    }
    return false;
  }

  onClick = () => {
    this.setState({});
  };

  render() {
    console.log('렌더링', this.state);

    return (
      <div>
        <button onClick={this.onClick}>렌더 테스트 클래스</button>
      </div>
    );
  }
}
// function Test() {
//   const [counter, setCounter] = useState(0);

//   const onClick = () => {
//     setCounter(0);
//   };

//   console.log('렌더링', counter);

//   return (
//     <div>
//       <button onClick={onClick}>렌더 테스트</button>
//     </div>
//   );
// }
export default Test;

리렌더링이 되는지 확인하는 방법 (react devtool 사용)

Untitled

크롬 익스텐션에서 react devtool을 다운 받으면 개발자 도구에서 components를 확인할 수 있다. 설정에서 components render될 때 하이라이팅 해주는 설정을 enable하면 왼쪽 그림처럼 리렌더링 되는 위치를 하이라이팅 해준다. 이 색이 빨간색에 가까워지면 리렌더링이 많다는 것이고, 이를 최적화 해주지 않으면 프로젝트가 커졌을 때 성능 문제가 생긴다.

성능 문제가 생기고, 최적화를 해야 할 때 이 렌더 하이라이팅 기능을 활용해서 최적화를 진행하면 되겠다.


PureComponent

import React, { PureComponent, useState } from 'react';

// PureComponent로 바꾸면 shouldComponentUpdate가 없어도
// 리렌더링 문제가 안생긴다.
class Test extends PureComponent {
  state = {
    counter: 0,
  };

  onClick = () => {
    this.setState({});
  };

  render() {
    console.log('렌더링', this.state);

    return (
      <div>
        <button onClick={this.onClick}>렌더 테스트 클래스</button>
      </div>
    );
  }
}

export default Test;

주석 처리 되어 있는 것 처럼, Component를 PureComponent로 바꾸면 shouldComponentUpdate() 없이 리렌더링 문제가 해결된다.

대신 {}, [] 와 같이 참조형 변수가 state로 저장된다면, 함수 컴포넌트에서 처럼 동일한 참조값이 있으면 동일한 것으로 인지해서 리렌더링이 되지 않는다.

여기까지 쓰고 보니 PureComponent가 함수 컴포넌트의 Default 설정인것 같다.

Untitled

함수 컴포넌트와 PureComponent의 비교를 명확히 해둔 부분이 없어서 정확히 말할 순 없지만 “주의” 부분의 PureComponent는 컴포넌트의 하위 트리에 대한 props 갱신 작업을 수행하지 않습니다. 라는 말이 있는걸로 보아선 함수컴포넌트와 PureComponent는 다른 방식으로 설계되어 있다고 유추할수 있다.

함수 컴포넌트에서 state가 변경이 되고 자식에 넘겨주는 props의 상태값이 변경되면 자식 컴포넌트의 하위 트리에 대한 props갱신이 될텐대, PureComponent는 안된다고 나와 있으니 둘은 다르다고 유추했다.

그리고 구글링해보면 리액트 16이전의 설명인 함수 컴포넌트는 상태값이 없을때 사용하는것이라는 설명이 많은데, 솔찍히 “뭔 말도 안되는 옛날 이야기를 하는거지?” 라는 생각이 많이 든다. 정말 블로그는 믿을게 못된다. 공식 문서를 믿자. 사실 공식문서 보다 더 믿을 만한건 깃헙 코드라고 하더라.


React.memo

위에 주저리 주저리 적었는데, 다음 내용으로 React.memo가 나오면서 바로 모든 내용이 해결되었다. PureComponent를 사용하면 하위 컴포넌트의 props가 변경되지 않아도 하위 컴포넌트가 리렌더링 되는 걸 막을 수 있는데 이를 위해 하위 컴포넌트에도 PureComponent와 같은 선언을 해주어야 한다.

예시 코드에서 하위 컴포넌트가 함수 컴포넌트라서 React.memo를 사용해볼 예정이다.

Untitled

React.memo로 래핑하지 않으면 상위 컴포넌트 리렌더링에 의해 <li> 태그도 리렌더링 되고 있다.

Untitled

아래 코드와 같이 React.memo(Try)로 래핑하면 props가 유지되는 상황에서 억울하게 리렌더링 되는 케이스를 막을 수 있다.

다만 이는 비교하는 과정을 거치기에 props가 유지되지만 리렌더링 되는 경우가 많이 발생할 때만 사용하고, 이외에 경우에는 성능상 이점이 없거나 오히려 악화 될 수도 있다는 걸 알고 적절하게 사용해야 한다.

import React, { memo } from 'react';

function Try({ tryInfo }) {
  const { id, input, result } = tryInfo;

  return (
    <li>
      {id}번째 결과 : "input: {input}" {result}
    </li>
  );
}

// export default Try;

// React.memo로 래핑하면 props가 같을 때 상위 컴포넌트 리렌더링에 의해
// 억울하게 자식도 리렌더링 되는 걸 막을 수 있다.
export default React.memo(Try);

위 내용이 정확한지 아직 확신이 없습니다. 다만 현재 공부한 내용을 정리하면 위의 결론이 나옵니다. 앞으로 더 알아봐야겠습니다.


React.createRef()

Class Component와 function Component에서 Ref를 만들 때 차이가 있다. 이 차이를 좁히기 위해 클래스 컴포넌트에서 inputRef = React.createRef() 이런 식으로 inputRef = useRef() 처럼 만들어 줄 수 있다.


Props 받아서 변경하기

import React, { useState } from 'react';

function Try({ tryInfo }) {
  const { id, input, result } = tryInfo;

  const [changedResult, setChangedResult] = useState(result);

  return (
    <li>
      <p>
        {id}번째 결과 : "input: {input}" {result}
      </p>
      <p onClick={() => setChangedResult(1)}>{changedResult}여기클릭</p>
    </li>
  );
}

// export default Try;

// React.memo로 래핑하면 props가 같을 때 상위 컴포넌트 리렌더링에 의해
// 억울하게 자식도 리렌더링 되는 걸 막을 수 있다.
export default React.memo(Try);

자식 컴포넌트에서 Props를 변경해선 안된다. 다만 꼭 그래야 하는 경우가 있으면 props를 state에 받아서, state를 변경하는 방식으로 해야 한다.

댓글