[React] 성능 개선과 반복문에서 주의해야할 것

jangwonyoon·2021년 8월 10일
0
post-thumbnail

웹 게임을 만들며 배우는 React 3강

강의 내용을 정리한 포스팅입니다.

📌 import와 Require 비교

exports되는 것이 객체나 배열이라면 구조분해 할 수 있습니다.

export const hello = 'hello'; // import { hello }

export defalut NumberBaseball; // import NumberBaseball;

default는 한번만 사용이 가능하고 const 선언은 여러번 사용이 가능하다.

babel이 Require를 import로 바꿔준다.

React는 import나 export를 사용한다. Noderequire을 사용한다. 하지만 어느정도 import와 Require 호환이 된다.

📌 리액트 반복문 Map, Key

react 반복문을 사용하는 방법은 Map 함수를 사용하여 반복문을 사용한다.

render() {
    return (
      <>
        <h1>{this.state.result}</h1>
        <form onSubmit={this.onSubminForm}>
          <input maxLength={4} value={this.state.value} onChange={this.onChangeInput} />
        </form>
        <div>시도 : {this.state.trires.length}</div>
        <ul>
          {['장원','혜준','원영','혜리','의연'].map((v,i) => {
            return (
              <li key={i}>{v}</li>
            )
          })}
        </ul>
      </>
    )
  }

만약 바뀌는 부분이 하나가 아닌 두 개인 경우는 이차원 배열을 만들어서 사용한다.

  • React에서의 이차원 배열
render() {
    return (
      <>
        <h1>{this.state.result}</h1>
        <form onSubmit={this.onSubminForm}>
          <input maxLength={4} value={this.state.value} onChange={this.onChangeInput} />
        </form>
        <div>시도 : {this.state.trires.length}</div>
        <ul>
          {[
						['사과', '맛없다'],
						['바나나','맛있다'],
						['귤', '사다']
						].map((v,i) => {
            return (
              <li key={i}><b>{v[0]}</b> - {v[1]}</li>
            )
          })}
        </ul>
      </>
    )
  }

또는 배열안에 객체를 넣어서 표현한다.

  • React에서 객체를 통한 반복문
render() {
    return (
      <>
        <h1>{this.state.result}</h1>
        <form onSubmit={this.onSubminForm}>
          <input maxLength={4} value={this.state.value} onChange={this.onChangeInput} />
        </form>
        <div>시도 : {this.state.trires.length}</div>
        <ul>
          {[
						{fruit:'사과', taste:'맛있다'},
						{fruit:'감', taste:'사다'},
						].map((v,i) => {
              <li key={i}><b>{v.fruit}</b> - {v.taste}</li>
            )
          })}
        </ul>
      </>
    )
  }

key는 화면에 표시는 안되지만 React에서 성능 최적화하는데 key를 사용하여 성능 최적화 하기 때문에, React의 반복문에서 key를 사용해야한다. key는 고유한 값을 선언 해주어야한다.

중요

key에 i를 넣는 경우가 많은데 key를 사용하는 이유가 성능 최적화이기때문에 i 말고 유의미한 값을 집어 넣어 주어야한다. 또한 key를 기준으로 엘리먼트를 추가 하거나 수정, 삭제를 판단하기 때문에 배열의 i로 집어 넣었을 때 순서 배열의 순서가 바뀌면 문제가 발생한다.

<li key={i}><b>{v.fruit}</b> - {v.taste}</li> -> i를 넣은 부분
<li key={v.fruit + v.taste}><b>{v.fruit}</b> - {v.taste}</li>

결론 : React의 반복문에서는 map을 사용하고, key를 반드시 넣어주어야한다. key는 idx가 아닌 고유한 값으로 넣어주어야한다. 성능 최적화와 다른 에러를 방지하기 위해

📌 컴포넌트 분리와 props

  • 성능과 가독성 측면이 있다.
  • 컴포넌트를 사용하는 이유는 재사용성에 있다.
  • 보통은 컴포넌트를 반복문 단위로 나눈다.
  • 처음에는 하나의 컴포넌트를 만들어 쪼개는 식으로 한다. → 초보자는 탑-다운 방식, 숙련자 다운 - 탑

✅ 메서드 바인딩

  • class 컴포넌트에서 화살표 함수로 사용하지 않은 경우 this를 사용하지 못한다.

  • 함수 선언식으로 사용하고 싶다면 class component안에 consturctor를 적어주어야한다.

  • 그와 더불어 함수를 바인딩 해야한다.

  • 화살표 함수가 아닌 함수 선언식으로 한 경우

    import React, { Component } from "react";
    import Try from './Try';
    function getNumbers() { // 숫자 네 개를 겹치지 않고 랜덤하게 뽑는 함수
    
    }
    
    class NumberBaseball extends Component {
      constructor(props) {
        super(props);
        this.state = {
          result: '',
          value: '',
          answer: getNumbers(),
          trires: [],
        };
        this.onSubminForm = this.onSubminForm.bind(this); // bind this
        this.onChangeInput = this.onChangeInput.bind(this);
      }
      
  onSubminForm(e) {
    e.preventDefalut();
  }

  onChangeInput(e) {
    this.setState({
      value: e.target.value
    })
  }

  fruits =
    [
      { fruit: '사과', taste: '맛있다' },
      { fruit: '감', taste: '사다' },
      { fruit: '귤', taste: '맛없다' },
      { fruit: '사과', taste: '맛이까?' },
    ];

  render() {
    return (
      <>
        <h1>{this.state.result}</h1>
        <form onSubmit={this.onSubminForm}>
          <input maxLength={4} value={this.state.value} onChange={this.onChangeInput} />
        </form>
        <div>시도 : {this.state.trires.length}</div>
        <ul>
          {this.fruits.map((v, i) => {
            return (
              <Try key={v.fruit + v.taste} value={v} index={i} />
            )
          })}
        </ul>
      </>
    )
  }
}

export default NumberBaseball;
```
  • 화살표 함수인 경우

    class NumberBaseball extends Component {
      this.state = {
        result: '',
        value: '',
        answer: getNumbers(),
        trires: [],
      };
    
      onSubminForm = (e) => {
        e.preventDefalut();
      }
    
      onChangeInput = (e) => {
        this.setState({
          value: e.target.value
        })
      }
    
      fruits =
        [
          { fruit: '사과', taste: '맛있다' },
          { fruit: '감', taste: '사다' },
          { fruit: '귤', taste: '맛없다' },
          { fruit: '사과', taste: '맛이까?' },
        ];
    
      render() {
        return (
          <>
            <h1>{this.state.result}</h1>
            <form onSubmit={this.onSubminForm}>
              <input maxLength={4} value={this.state.value} onChange={this.onChangeInput} />
            </form>
            <div>시도 : {this.state.trires.length}</div>
            <ul>
              {this.fruits.map((v, i) => {
                return (
                  <Try key={v.fruit + v.taste} value={v} index={i} />
                )
              })}
            </ul>
          </>
        )
      }
    }

    화살표 함수는 bind this 를 자동으로 처리해준다.

    리액트에서는 배열에 push를 사용해서는 안된다.

    • 리액트는 예전 상태와 현재 상태가 바뀌어야 React에서 감지 할 수 있다.

    • 리액트는 어떤것이 바뀌었는지 감지할 수가 없다. (React의 불변성)

    • 감지 할 수 있도록 기존 배열을 복사하고 새로운 값을 넣어준다.

      ex)
      
      const arr = [];
      
      const arr2 = [...arr, 2]; // 새로운 상태 만들기, ... 전개 연산자 사용
      
      arr === arr2 // false
      

✅ 함수의 분리

  • this를 사용하지 않는 경우는 함수를 class 내부가 아닌 밖으로 빼서 사용한다.

📌 React Devtools 성능향상

class 컴포넌트에서 성능향상

  • setState만 호출하면 상태가 바뀐다. 기존에 상태가 바뀌어야만 렌더링이 될거라 생각하지만 React가 멍청하기 때문에 어떤 경우에 렌더링이 되어야할지 설정해주어야한다.

  • shouldComponentUpdate

    shouldComponentUpdate통해 조건부 렌더링 한다.

    조건부 렌더링을 통해 성능 개선을 한다.

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

PureComponent

  • shouldComponentUpdate 기능을 처리해주는 컴포넌트

✅ Hooks를 통한 렌더링 성능개선 React.memo

react Hooks에서 성능향상

import React, { memo } from 'react';

const Try = memo(({ tryInfo }) => {
  return (
    <li>
      <div>{tryInfo.try}</div>
      <div>{tryInfo.result}</div>
    </li>
  )
});

export default Try;

📌 React.createRef

두 가지 방법이 구분된다. React.createRef

  • React.createRef을 사용한 방식
import React, { Component, createRef } from 'react';

class NumberBaseball extends Component {
	input = createRef();

	this.inputRef.current.focus();// 이런식으로 작성
}

추가 알면 좋을것

setState를 함수로 표현해서 처리하면 더 미세한 동작을 할 수 있다. 1급 함수, 객체, HOC 고차 컴포넌트

Ex) 함수 안에 다른 함수를 넣는 것 HOC 고차 컴포넌트

this.setState((prevState) => {
	return {
			result: '홈런!',
				tries: [...prevState.tries, { try: value, result: '홈런!' }],
	}});
  • 렌더 안에서 this.setState를 사용하면 안된다.
  • this.setState를 사용하게 되면 render함수를 호출하고 렌더 함수를 호출하면 다시 setState를 호출하게 되기 때문에 무한 호출이 생기게 된다.

📌 props와 state 연결하기

  • props는 부모가 바꾸어야지 자식이 바꾸면 안된다. (불변성)

  • 실무에서는 간혹 부모로 받은 props를 바꾸어야하는 경우가 생긴다.

    • 이러한 경우 props를 자식 컴포넌트에서 state바꾸어 처리한다.

    • 예시

      const Try = memo(({ tryInfo }) => {
        const [result, setResult] = useState(tryInfo.result); // props를 useState를 통해 상태로 만들어준다.
      
        const onClick = () => {
          setResult('1')
        }// 상태를 바꾸어서 props를 만진다.
      
        return (
          <li>
            <div>{tryInfo.try}</div>
            <div>{tryInfo.result}</div>
          </li>
        )
      });
      
      export default Try;

📌 React.memo()

  • react는 먼저 컴포넌트를 렌더링 한 뒤, 이전 렌더된 결과와 비교하여 DOM 업데이트를 결정한다. 만약 렌더 결과가 이전과 다르다면, React는 DOM을 업데이트 한다.

  • 컴포넌트가 React.memo() 로 래핑 되는 경우, React는 컴포넌트를 렌더링하고 결과를 메모이징(Memozing)한다. 그 후 다음 렌더링이 일어는 경우 props 가 같다면, React는 메모이징된 내용을 재사용한다.

    React.memo 를 통한 기대효과

    메모이징 한 결과를 재사용 함으로써, React에서 리렌더링을 할 때 가상 DOM에서 달라진 부분을 확인하지 않아 성능상의 이점을 누릴 수 있다.

✅ props 동등 비교 커스터마이징

React.memo() props혹은 props의 객체를 비교할 때 얕은 비교를 한다. 비교 방식을 수정하고 싶다면 React.memo() 두 번째 매개변수로 비교함수를 만들어 넘겨주면 된다.

React.memo(Component, [areEqual(prevProps, nextProps)]);

areEqual(prevProps, nextProps) 함수는 prevProps와 nextProps가 같다면 true를 반환할 것이다.

✅ 언제 React.memo() 를 사용해야할까?

React.memo() 는 함수형 컴포넌트에 적용되어 같은 props에 같은 렌더링 결과를 제공한다.

React.memo() 를 사용하기 가장 좋은 케이스는 함수형 컴포넌트와 같은 props로 자주 렌더링 될거라 예상될 때이다.

일반적으로 부모 컴포넌트에 의해 하위 컴포넌트가 같은 props로 리렌더링 될 때가 있다.

컴포넌트가 같은 props로 자주 렌더링되거나, 무겁고 큰 연산이 있는 경우, React.memo() 로 컴포넌트를 래핑 할 필요가 있다.

✅ 언제 React.memo()를 사용하지 말아야할까?

성능적으로 이점을 얻지 못하는경우, 메모이제이션을 사용하지 않는 것이 좋다.

성능 관련 변경이 잘못 적용 된다면 성능이 오히려 악화될 수 있다.

기술적으로는 가능하지만 클래스 기반의 컴포넌트를 React.memo() 로 래핑하는 것은 적절하지 않다.
클래스 기반의 컴포넌트에서 메모이제이션이 필요하다면 PureComponent 를 확장하여 사용하거나, shouldComponentUpdate() 생명주기 메서드를 통해 구현하는 것이 적절하다.

쓸모없는 props 비교

렌더링 될때 props 가 다른 경우가 대부분인 컴포넌트를 생각해보면, 메모이제이션 기법의 이점을 얻기 힘들다.

props 가 자주 변하는 컴포먼트를 React.memo() 로 래핑할지라도, React는 두 가지 작업을 리렌더링 할때마다 수행할 것이다.

  1. 이전 props와 다음 props의 동등 비교를 위해 비교 함수를 수행한다.
  2. 비교 함수는 거의 항상 false를 반환할 것이기 때문에, React는 이전 렌더링 내용과 다음 렌더링 내용을 비교할 것이다.

비교 함수의 결과는 대부분 false 를 반환하기에 props 비교는 불필요하게 된다.

📌 React.memo()는 성능 개선의 도구이다.

React에서는 성능 개선을 위한 하나의 도구로 메모이제이션을 사용한다.

대부분의 상황에서는 React는 메모이징 된 컴포넌트의 리렌더링을 피할 수 있지만, 렌더링을 막기 위해 메모이제이션에 의존해서는 안된다.

📌 결론

React.memo()는 함수형 컴포넌트에서도 메모이제이션의 장점을 얻게 해 주는 훌륭한 도구이다. 콜백 함수를 props로 사용하는 컴포넌트에서는 메모이징을 할 때 주의해야한다. 그리고 같은 렌더링을 할 때 이전과 동일한 콜백 함수 인스턴스를 넘기는지 확실히 해야한다.

📕 레퍼런스

profile
어제보다 오늘 더 노력하는 프론트엔드 개발자

0개의 댓글