[React] useCallback과 useMemo 제대로 사용하기
Front-End/React

[React] useCallback과 useMemo 제대로 사용하기

 

 

 

 

 

 

useCallbackuseMemo는 메모이제이션 된 값을 반환한다. 차이점은 useCallback은 함수를 메모이제이션하고 useMemo는 값을 메모이제이션한다.

 

면접을 준비하여 외웠던 대답이다. 실제 프로젝트를 하면서 useCallback을 써봤지만 왜 쓰는지, 언제 써야하는지를 정확히 이해하지 못했었다.(심지어는 전혀 이점없는 곳에 썼다🙄) 회사에 와서야 useCallbackuseMemo를 사용하는 목적이 무엇인지를 완전히 이해하고 올바르게 사용하게 되었다. 이제 내가 이해한 내용을 여기에 정리해보려한다.

 

 

 

 

REACT의 리렌더링

 

 

React가 리렌더링을 하는 조건은 3가지이다.

  • 자신의 state가 변경될 때
  • 부모 컴포넌트로부터 전달받은 props가 변경될 때
  • 부모 컴포넌트가 리렌더링 될 때

 

 

여기 간단한 앱이 있다.

// App.js

function App() {
  const [number, setNumber] = useState(0);
  return (
    <div className="App">
      <div className="num" onClick={()=>{setNumber(number+1)}}>{number}</div> 
      <Button/>
    </div>
  );
}

export default App;

숫자를 클릭하면 숫자가 1씩 증가하는 앱이다. 아래에는 버튼을 만들어 주었다.

 

실행한 모습은 위와 같다.

 

 

Button컴포넌트는 props가 없는 순수 UI컴포넌트이다.

// Button.js

function Button() {
    return (
      <button className="button">RESET</button>
    );
  }

  export default Button;

이 컴포넌트는 항상 같은 결과를 return하지만 '부모 컴포넌트가 리렌더링 될 때 자식 컴포넌트로 리렌더링 된다.' 라는 조건에 따라 부모 컴포넌트가 리렌더링 될 때 항상 리렌더링된다.

 

 

 

 

 

물론 지금과 같이 매우 간단한 앱에서는 불필요한 리렌더링이 몇번이고 일어나든 성능에 문제되지 않는다. 하지만 만약 Button이라는 컴포넌트가 엄청 복잡하고 값비싼 코드를 포함하는 컴포넌트라면 어떨까?  리렌더링을 최대한 줄여야할 것이다. 
그렇다면 이 불필요한 리렌더링을 막을 방법은 무엇일까?

 


바로 React.memo를 사용하면 된다.

 

 

 

 

 

👀React.memo를 알아보자

 

React.memo컴포넌트를 메모이제이션해준다.

부모 컴포넌트로 넘겨받는 props가 같다면 메모이제이션 해둔 렌더링 결과를 가져온다. 메모이제이션한 내용을 재사용하여 렌더링시 가상 DOM에서 달라진 부분을 확인하지 않아 성능상의 이점이 생기게 된다.

const MyComponent = React.memo(function MyComponent(props) {
  /* props를 사용하여 렌더링 */
});
function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
export default React.memo(MyComponent, areEqual);

위와 같이 컴포넌트를 감싸주는 방식으로 사용하면 된다.

 

 

그럼 Button 컴포넌트를 React.memo로 감싸보자.

// Button.js

function Button() {
    return (
      <button className="button">RESET</button>
    );
  }

  export default React.memo(Button);

 

이제 버튼 부분은 리렌더링 되지 않는것을 확인할 수 있다.

 

 

 

 

 

 

현재 버튼은 아무런 동작도 일으키지 못한다.

버튼이 제역할을 하기위해 RESET버튼을 클릭하면 숫자가 0으로 리셋되도록 구현해주었다.

// App.js

const onClick = () => {
    setNumber(0);
  }

  return (
    <div className="App">
      <div className="num" onClick={()=>{setNumber(number+1)}}>{number}</div> 
      <Button onClick={onClick}/>
    </div>
  );

onClick함수를 만들고 Button컴포넌트에 넘겨주었다.

// Button.js

function Button({onClick}) {
    return (
      <button className="button" onClick={onClick}>RESET</button>
    );
  }

  export default React.memo(Button);

 

 

이제 다시 앱을 실행시켜서 버튼이 제 역할을 잘 하는지 확인해보자. 버튼은 잘 동작한다. 그런데 숫자를 클릭하면 버튼이 다시 리랜더링 되는 현상을 발견할 수 있다.

 

 

 

 

React.memo가 제 기능을 못하는 걸까🤔?

 

 

 

 

 

 

object !== object

 

리렌더링이 발생되면 해당 컴포넌트의 모든 객체들은 다시 생성된다.( ❗ 함수도 객체이다. ) javascript에서 객체는 참조타입으로 완전히 동일한 값을 가지고 있더라도 참조하는 주소가 다르면 서로 다른 객체로 취급된다.

(참고: 값과 레퍼런스)

 

b는 a의 주소값을 복사했기 때문에 동일한 객체라 인식하지만,

a와 값이 동일한 객체는 a와 주소값이 다르기 때문에 다른 객체라 판단한다.

 

즉, 컴포넌트는 리렌더링할 때 마다 새로운 함수를 계속 생성하며, React.memo는 부모 컴포넌트로 넘겨받는 props가 변경되었다고 판단하여 계속 리렌더링 하는 것이다.

 

 

 

useCallbackuseMemo 는 여기서 발생하는 불필요한 렌더링과 불필요한 계산을 방지하는 목적으로 설계되었다.

 

 

 

 

 

 

useCallback

 

 

useCallback 은 언제나 동일한 함수를 return해준다.

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b], // deps
);

deps안에 넣어준 값이 바뀔때에만 새로운 객체를 생성한다.

 

React.memo로 감싸준 자식 컴포넌트에게 함수를 prop로 넘겨줄 경우, 넘겨받는 함수를 useCallback으로 감싸주면 deps 가 바뀔 경우를 제외하고 항상 동일한 객체를 넘겨줌으로 불필요한 리렌더링을 방지할 수 있다.

 

 

 

// App.js

function App() {
  ...
  const onClick = useCallback(() => {
      setNumber(0);
    },[]);
    ...
}

Button컴포넌트에 넘겨주는 onClick 함수를 useCallback으로 감싸주게 되면, Button의 불필요한 리렌더링이 방지된다.

 

 

 

 

 

 

 

 

useMemo🗒

 

 

useMemouseCallback과 동일한 방식으로 사용하면 된다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

 

 

Button이 공통컴포넌트이고 상황에 따라 style을 커스텀 할 수 있게 props로 style을 넘겨준다고 가정해보자.

// App.js

function App() {
  ...
  return (
      <div className="App">
        <div className="num" onClick={()=>{setNumber(number+1)}}>{number}</div> 
        <Button onClick={onClick} style={{backgroundColor: 'darkseagreen'}}/>
      </div>
    );
    ...
}

react에서 인라인으로 객체를 넣으면 위에서 설명했듯이, 리렌더링시마다 새로운 객체가 생성된다. 따라서 Button은 계속 리렌더링 된다.

 

 

 

이때, useMemo를 사용해서 객체를 메모이제이션 해줌으로 style prop에 대해 동일한 참조를 제공할 수 있다.

// App.js

function App() {
  ...
	const buttonStyle = useMemo(() => ({backgroundColor: 'darkseagreen'}), []);

  return (
    <div className="App">
      <div className="num" onClick={()=>{setNumber(number+1)}}>{number}</div> 
      <Button onClick={onClick} style={buttonStyle}/>
    </div>
  );
  ...
  
 }

 

 

deps가 빈 배열인 이유는 위의 예시에서는 해당 함수나 값이 의존하는 변수가 없기 때문이다. 의존하는 값이 존재하는 경우에는 반드시 deps 배열안에 명시해줘야 한다.

 

 

 

 

 

 

 

 

 

📌

https://alexsidorenko.com/blog/react-render-usememo/