[React] 최적화 잘 이해하고 있나요?

Joo·2024년 3월 15일

React

목록 보기
7/11
post-thumbnail

++ 리액트를 더 잘 쓰기 위한 정리 시리즈

리액트 앱의 최적화 가 필요한 이유

컴포넌트가 많고 복잡한 앱일수록 최적화가 더욱 필요하다.
여러 하위 컴포넌트를 중첩한 경우 상위 컴포넌트에서 새로운 상태로 업데이트가 되면 하위 컴포넌트들도 렌더링이 된다. 불필요한 컴포넌트들이 렌더링이되고, 그 수가 많을수록 당연히 성능 저하로 이어진다.

최적화를 할 수 있는 상황과 방법을 예제를 통해 알아보자!

memo API

memo 는 컴포넌트를 감싸는 최적화 API로, 컴포넌트의 props 속성값이 바뀌지 않으면 업데이트가 필요하지 않는 컴포넌트에 사용할 수 있다. (마치 y = x 와 같은 선형 함수에서 입력값이 바뀌지 않으면 출력도 바뀌지 않는 것처럼)

+ 버튼을 누르면 counter가 증가하고 - 버튼을 누르면 counter가 감소하는 기능을 생각해보자.
아래 코드는 버튼의 내용과 스타일의 틀을 만들어주는 컴포넌트일 뿐이다. counter의 증가와 감소와는 사실 관련이 없다.

// memo 적용 전
export default function IconButton({ children, icon, ...props }) {

  const Icon = icon;
  return (
    <button {...props} className="button">
      <Icon className="button-icon" />
      <span className="button-text">{children}</span>
    </button>
  );
}

IconButton 컴포넌트는 props 속성으로 { children, icon, ...props} 가 필요한데 아래 코드처럼 icon 도 고정, children 인 Decrement, Increment도 고정, 나머지 ...props 인 onClick에 사용하는 함수도 변경되는 로직이 아니다.

// InconButton 컴포넌트를 사용하는 Counter 컴포넌트
<IconButton icon={MinusIcon} onClick={handleDecrement}>
	Decrement
</IconButton>
<CounterOutput value={counter} />
<IconButton icon={PlusIcon} onClick={handleIncrement}>
	Increment
</IconButton>

그렇다면 우리는 IconButton 컴포넌트를 최적화 할 수 있다!
아래 코드처럼 memo 를 import 하고, 컴포넌트를 감싸주기만 하면 된다.

import { memo } from "react";

const InconButton = memo(function IconButton({ children, icon, ...props }) {

  const Icon = icon;
  return (
    <button {...props} className="button">
      <Icon className="button-icon" />
      <span className="button-text">{children}</span>
    </button>
  );
});

export default InconButton;

memo를 적용한 후 컴포넌트 렌더링이 어떻게 진행되는지 콘솔로 확인해보면

아직도 IconButton 컴포넌트가 렌더링된다..
그렇다면 렌더링이 필요한 이유인 props 값의 변화가 있었다는 말이다.

useCallback 훅

위에서 설명했듯, icon, children 속성은 고정되어 있다.
그러면 의심해볼만한건 나머지 ...propsonclick에 정의한 함수 밖에 없다.

함수가 변경되는 것도 아닌데 왜?? 라는 생각이 들었지만 JS에서 함수는 곧 객체로 만들어졌고, Counter 컴포넌트가 렌더링되면서 함수는 매번 새롭게 생성되며 정의된다. 즉, 이전 함수와 생김새와 역할은 같은데 다른 함수라는 것이다.

이 때, useCallback 함수를 사용할 수 있다.
react 에서 useCallback 훅을 import 한 뒤 최적화하려는 함수를 감싸고 의존성 배열을 설정해주면 된다.

import { useCallback } from "react";

const handleDecrement = useCallback(function handleDecrement() {
	setCounter((prevCounter) => prevCounter - 1);
}, []);

const handleIncrement = useCallback(function handleIncrement() {
	setCounter((prevCounter) => prevCounter + 1);
}, []);

의존성 배열에 대한 더 자세한 설명은 참고문헌을 확인해주세요!
https://react-ko.dev/reference/react/useCallback

함수까지 최적화를 하고 난 뒤, Counter 컴포넌트 다음에 IconButton 컴포넌트가 렌더링되지 않은 것을 확인할 수 있다!

useMemo 훅

useMemo 훅은 useCallback 훅과 비슷하다. useCallback이 함수 자체를 새롭게 생성하는 것을 막는 것처럼 useMemo는 함수의 반환값을 그대로 사용할 수 있게 해준다.

만약 isPrime이라는 입력값이 소수이면 true를 소수가 아니면 false를 리턴해주는 함수를 생각해보자. 입력에 10000, 100000 ... 점점 큰 값을 넣을경우 값을 계산하는데 시간이 오래걸리면 최적화 성능에 영향을 주게 된다.

function isPrime(number) {
  if (number <= 1) {
    return false;
  }

  const limit = Math.sqrt(number);

  for (let i = 2; i <= limit; i++) {
    if (number % i === 0) {
      return false;
    }
  }

  return true;
}

그래서 useMemo를 사용해 입력값이 변함 없다면 함수의 반환값도 변함 없으니까 복잡한 계산을 다시 안하도록 해줄 수 있다. useCallback 처럼 의존성 배열에 initialCount 값을 넣어주었다. 입력값인 initialCount가 변하면 함수를 실행해서 결과를 가져와야하기 때문이다!

import { useMemo } from "react";

const initialCountIsPrime = useMemo(
  () => isPrime(initialCount),
  [initialCount]
);

정리

  • memo는 props값이 변화하지 않은 이상 컴포넌트를 렌더링하지 않아 앱의 성능을 높여줄 수 있는 함수다.

  • useCallback은 불필요한 함수의 재생성을 방지하고 해당 함수가 props로 사용되는 컴포넌트의 최적화에 영향을 준다.

  • useMemo는 useCallback처럼 시간 복잡도가 높을 수 있는 함수의 실행을 필요한 경우만 사용할 수 있게 해준다. 반환값의 재사용

⛔️ 주의할 점

최적화를 적용할 때는 이 작업이 필요한지 생각해봐야 한다.🤔

memo 의 경우 props에 해당하는 속성들이 변화가 있는지 리액트가 체크하는 과정이 필요하다. props값이 자주 변경되고 업데이트가 필요한 컴포넌트라면 굳이 memo를 사용해 props가 변경되는지 체크하는 과정들을 더 넣는 것이 최적화에 유의미한지 고민해봐야 한다.

함수의 재생성은 이 자체로 앱에 큰 부담을 주지 않는다고 한다. 리소스를 크게 사용하지 않아서.. 즉, useCallback 자체로 사용하는 것은 큰 의미가 없을 수도 있다.. 하지만 memo와 함께 컴포넌트 렌더링의 최적화를 위해 사용한다면 중요한 훅이다.

이번에 최적화에 대해 더 깊이 이해할 수 있던 시간이라고 생각한다. 물론 전부 이해하진 못했지만,, 실제 프로젝트를 하면서 생각없이 최적화 훅을 사용하는 것보다 이유를 생각하면서 코드를 짜는데 큰 도움이 될 것이라고 생각한다 😀

profile
한 줄이 모여 책이 되듯 기록하기

0개의 댓글