shouldComponentUpdate 을 구현하는 것이 복잡하다면 extends Component 대신 extends PureComponent를 하면 된다. PureComponent는 shouldComponentUpdate를 알아서 구현한 컴포넌트라고 한다.

 

PureComponent의 원리는 state들이 바뀌었는지 안바뀌었는지 판단해서 shouldComponent에 return true를 할지 false를할지 결정한다. 하지만 state가 array이거나 object라면 불변성을 지켜야만 변경점을 알아차리고 리렌더링을 해준다. (이건 일반 Component도 마찬가지)

 

강의에서 말하고 싶었던 것 : {a:1}에서 setState {a:1}을 할 때 새로 렌더링하므로 state에 객체 구조를 안쓰는게 좋다고한다.

(배열안에 객체넣고 그 안에 배열넣고 이렇게 자료구조를 짜는 것은 적합하지 않다고함.)

 

클래스 컴포넌트의 PureComponent 정리

1. PureComponent는 state가 바뀌었는지 안바뀌었는지를 알아차리기 때문에 바뀌는 경우에만 리렌더링을 한다. (그러므로 PureComponent 를 자주 써라) 

2. state가 객체나 배열이라면 불변성을 지키기 위해 spread 연산자를 사용해서 매번 새로운 배열, 객체를 만들어주자.

 

 

3-9번 부터의 목표

숫자야구에서 input창에 글을 쓰면 <TryHooks /> 컴포넌트까지 리렌더링이 되어서 성능 낭비가 발생한다.

부모 컴포넌트가 리렌더링 되면서 자식 컴포넌트까지 리렌더링 되는 상황인데 이것을 해결하고자 한다.

 

React.memo

그렇다면 어떻게 해결해야할까?

<TryHooks /> 는 클래스형 컴포넌트가 아니라 함수 컴포넌트라서 PureComponent을 사용할 수 가 없다.

함수 컴포넌트에서도 PureComponent 와 같은 역할을 해주는 것이 있는데 React.memo를 사용하면 된다.

 

memo의 역할은 부모 컴포넌트가 리렌더링 됐을 때 자식 컴포넌트가 리렌더링 되는 것을 막아준다.대신에 state나 props가 바뀌었을 때는 여전히 리렌더링 된다. (당연히 되어야하는 것이기도 하다.)

import React, {memo} from "react";

const TryHooks = memo(({index, value, test}) => {
    return (
        <li>
            {index}. <b>{value}</b> {test}
        </li>
    )
});
TryHooks.displayName = 'TryHooks'
export default TryHooks;

memo로 감싸주기만 하면 된다.

TryHooks.displayName = 'TryHooks'

해당 부분은 memo를 감싸면 개발자도구에서 보는 컴포넌트 이름이 이상하게 나오는데 그것을 우리가 설정한 이름으로 보이게 해주는 부분이다.

 

 

1. Props를 자주 사용하다 보면 생기는 문제

렌더링이 자주 일어나서 성능이 안좋아지는 문제가 있다.

 

해당 문제를 해결하기 위해서 크롬 확장 프로그램 중에 React Devtools를 설치해서 props나 state부분을 살펴볼 수 있다는 내용이었음..

 

리렌더가 되는 타이밍 : state나 props가 바뀌었을 때 (props가 state였을 때 변경된다.. 일반 변수 넘긴 후 변경해봤는데 안됨..)

 

강의 내용에서 클래스 컴포넌트로 테스를 해봤는데 리렌더가 되는 타이밍은 사실 this.setState가 호출되는 시점이었다. (state가 바뀌지 않은 상황이더라도 호출은 되었으니 리렌더가 일어남.)

 

이것을 방지하려면

shouldComponentUpdate

을 구현해서 이전값과 다를 때만 렌더링되도록 true를 반환하면 되는데 클래스컴포넌트 문법이라서 따로 따라하지는 않았다.

Hooks 변경한 코드

import React, {Component, useState, useRef} from "react";
import TryHooks from "./TryHooks";

function getNumbers(){
    const numbers = new Array(9).fill(0).map((v,i)=>v+i+1);
    const shuffle = [];
    while(numbers.length > 0){
        const rand = Math.floor(Math.random() * numbers.length);
        const number = numbers.splice(rand,1)[0];
        shuffle.push(number);
    }
    console.log(shuffle);
    return shuffle.slice(0,4);
}

const NumberBaseballHooks = () => {
    const [value, setValue] = useState('');
    const [result, setResult] = useState('');
    const [answer, setAnswer] = useState(getNumbers());
    const [tries, setTries] = useState([]);
    const inputRef = useRef(null);
    const onSubmitForm = e => {
        e.preventDefault();
        const answerStr = answer.join('');
        if(answerStr === value){
            //홈런
            setResult('홈런!');
            setValue('');
            setTries((prev)=>{
                return [...prev, `${value}:홈런!`]
            });
            setTimeout(()=>{
                alert('게임을 다시 시작합니다.');
                setResult('');
                setAnswer(getNumbers());
                setTries([]);
            },100);

        } else {
            if(tries.length >= 9){
                setResult(`10번 넘게 틀려서 실패! 정답은 ${answerStr} 였습니다`);
                setValue('');
                alert('게임을 다시 시작합니다.');
                setResult('');
                setAnswer(getNumbers());
                setTries([]);
            }else{
                //볼, 스트라이크 판단
                let ball = 0;
                let strike = 0;
                for(let i=0; i<4; i++){
                    if(answerStr[i] === value[i]){
                        strike++;
                    }else if(answerStr.includes(value[i])){
                        ball++;
                    }
                }
                setTries((prev)=>{
                    return [...prev, `${value}:${strike}S ${ball}B`];
                })
                setResult('땡');
                setValue('');
            }
        }
        inputRef.current.focus();
    }

    const onChangeInput = e => {
        const value = e.target.value;
        setValue(value);
    }

    return (
        <>
            <h1>{result}</h1>
            <form onSubmit={onSubmitForm}>
                <input type="text" maxLength={4} ref={inputRef} value={value} onChange={onChangeInput}/>
                <button>입력</button>
            </form>
            <div>시도: {tries.length}</div>
            <ul>
                {
                    tries.map((v,i) => {
                        return (<TryHooks key={v+i} value={v} index={i+1} />);
                    })
                }
            </ul>
        </>
    )
}
export default NumberBaseballHooks;

NumberBaseballHooks.jsx

 

import React from "react";

const TryHooks = ({index, value}) => {
    return (
        <li>
            {index}. <b>{value}</b>
        </li>
    )
}
export default TryHooks;

TryHooks.jsx

 

 

근데 여기서 문제가 있었다.

input에 value를 변경하면 setValue가 value state를 변경하면서 리렌더가 되는데 리렌더가 되면서 NumberBaseballHooks 내부의 로직이 재실행 되면서 getNumbers()까지 재호출 되는 문제가 있었다.

그래서 위의 사진과 같이 console.log가 input에 타자 칠 때마다 나오는 것.

 

const [answer, setAnswer] = useState(getNumbers());

해당 코드가 문제였는데 저렇게 한다고해서 state가 리렌더 할 때마다 바뀌는 것은 아니고 호출만 여러 번 되는것이다.

하지만 호출이 되는 것도 문제이다. 

useState가 자동으로 getNumbers()를 첫 번째 호출한 리턴값만 state에 저장하고 그 다음부터는 리턴값을 무시해버린다. 하지만 리렌더 시에 getNumbers함수가 계속 재호출 되긴한다. (호출값을 넣어버렸기 때문에) 만약 getNumbers가 아주 오래걸리는 작업을 하는 함수라면 매우 비효율적이다.

 

그럴 때는 아래와 같이 호출해서 넘기지 말고 함수 자체를 넘기면 된다. (lazy init 이라고 한다.)

const [answer, setAnswer] = useState(getNumbers);

 

useState 용법이 2가지고 있는데

1. useState(초기값) - 일반적으로 쓰이는 경우로 초기값으로 state를 설정해준다.

2. useState(함수) - 함수를 그대로 넣는 경우인데 함수의 리턴값이 초기값으로 설정이 되지만 그 다음부터 이 함수는 실행되지 않는다. 

+ Recent posts