React 리렌더링 memo와 useCallback

용인사는 개발자·2025년 8월 25일
post-thumbnail

Intro

사이드 프로젝트 진행하다가 발생한 문제는 아니고 문득 GPT에게 이것저것 질문을 하던중 usememo, usecallback, React.memo를 이용한 렌더링 최적화 주제가 나왔었다.

아차 싶었다..
내가 데이터를 받아서 컴포넌트를 뿌려줄때 해당 컴포넌트에 그런 처리를 해줬던가?
사이드 프로젝트에서 제일 많이 쓰이는 컴포넌트를 확인해봤다.

const TagItem = (props: AnyTagItemProps) => {
  return (
    <>
      <Badge
        theme={{
          root: {
            color: {
              gray: "bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600",
              cyan: "bg-cyan-100 text-cyan-800 hover:bg-cyan-200 dark:bg-cyan-200 dark:text-cyan-900 dark:hover:bg-cyan-300",
            },
          },
        }}
        color={
          props.mode == "check" ? (props.isChecked ? "cyan" : "gray") : "cyan"
        }
        size="md"
      >
        <button className="cursor-pointer" onClick={props.clickEvt}>
          {props.text}
        </button>
      </Badge>
    </>
  );
};

export default TagItem;

그런거 안되어있네..
React Developer Tools 을 사용하여 확인해보자.

TagItem 컴포넌트는 PostFilter의 자식으로 사용 중인데, 부모에서 내려주는 checked props 값이 바뀔 때마다 모든 TagItem이 함께 리렌더링되는 문제가 있었다.
그럼 어떻게 해야 할까?

TroubleShooting

처음에 말했던 내용처럼 React.memo를 사용하여 TagItem을

export default memo(TagItem, (prev, next) => {
  if (prev.mode !== next.mode) return false;
  if (prev.mode === "check" && next.mode === "check") {
    if (prev.isChecked !== next.isChecked) return false;
  }
  if (prev.text !== next.text) return false;
  if (prev.id !== next.id) return false;
  if (prev.body !== next.body) return false; // body 비교 추가
  if (prev.clickEvt !== next.clickEvt) return false;
  if (prev.clickValueType !== next.clickValueType) return false;
  return true;
});

이렇게 감싸주었고 clickEvt 함수 prop이라 부모 컴포넌트에서 해당 함수를 useCallback 처리를 해서 전달해주었다. 그랬더니 짜잔

TagItem을 라디오 버튼처럼 쓰고 있었는데, 생각해보니 체크가 바뀔 때 필요한 건 새로 선택된 애랑 기존에 선택 해제되는 애, 딱 두 개뿐이었다. 굳이 전부 리렌더링될 필요가 없었고, 체크된 거랑 해제된 거만 바꿔주면 충분했고 위 이미저처럼 만족스럽게 바뀌었다.

그럼 memo는 뭐길래 컴퍼넌트를 감싸주면 리렌더링이 방지되는 것일까? GPT에게 물어보자.

React.memo는 컴포넌트가 받은 props를 얕게(shallow) 비교해서 값이 안 바뀌면 이전 렌더링 결과를 그대로 재사용한다. 그래서 실제로 바뀐 props가 있을 때만 리렌더링이 일어난다.

그럼 useCallback은 왜쓰는걸까?

부모가 리렌더링되면, 그 안에서 새로 정의된 함수는 매번 새로운 참조(주소)를 갖게되고
이 함수를 자식 컴포넌트에 props로 넘기면, 자식 입장에서는 "props가 바뀌었다"고 판단해서 불필요하게 리렌더링될 수 있음.
useCallback으로 감싸면 함수 참조를 메모이제이션해서, deps가 변하지 않는 이상 같은 함수 객체를 계속 쓰게 된다.

Conclusion

부끄럽지만 내 사이드 프로젝트에서는 React.memo, useCallback, useMemo를 제대로 활용하지 못했었다. 예전에 설명과 예제를 흘려보듯 본 적은 있었지만 크게 와닿지는 않았었다. 그런데 이번 프로젝트에서 직접 적용해보면서, 이게 얼마나 중요한 부분이고 또 리액트에서 리렌더링 최적화를 얼마나 세심하게 신경 써야 하는지 뼈저리게 느끼게 되었다.

profile
용인에 거주하는 개발자입니다. 사이드프로젝트의 진행사항, 오류해결, 배운것들을 적습니다

0개의 댓글