React 성능 최적화 (Optimization)

chaen·2024년 4월 17일

REACT / NEXT.js

목록 보기
12/22
post-thumbnail

웹 서비스의 성능을 개선하는 모든 행위로, 아주 단순한 것부터 아주 어려운 방법까지 매우 다양합니다.

일반적으로 서버의 응답속도 개선, 이미지 / 폰트 / 코드 파일 등의 정적 파일 로딩 개선, 불필요한 네트워크 요청 줄임 등의 최적화가 있을 수 있습니다.

React 내부에서는 컴포넌트 내부의 불필요한 연산, 함수 재생성, 리렌더링 방지 등이 최적화의 주요 대상입니다.

이번 글에서는 React 내부에서 가능한 대표적인 최적화 기법들을 하나씩 살펴보겠습니다.


useMemo: 불필요한 연산 방지 (값)

useMemo메모이제이션(Memoization) 기법을 기반으로, 값의 재계산을 방지하여 성능을 최적화하는 훅입니다.

useMemo(() => {
  return value;
}, [item]);
  • 첫 번째 인수: 계산 로직이 담긴 콜백 함수
  • 두 번째 인수: 의존성 배열로, 배열 내 값이 변경될 때만 콜백이 다시 실행됩니다.

예시

const App = () => {
  const { totalCount, doneCount, yetCount } = useMemo(() => {
    const totalCount = todos.length;
    const doneCount = todos.filter((todo) => todo.isDone).length;
    const yetCount = totalCount - doneCount;

    return { totalCount, doneCount, yetCount };
  }, [todos]);

  return (
    <div>
      <div>total: {totalCount}</div>
      <div>done: {doneCount}</div>
      <div>yet: {yetCount}</div>
    </div>
  );
};

기존의 filter는 검색을 하거나 다른 버튼이 눌릴 때도 다시 계산되었겠지만, useMemo로 감쌈에 따라todos가 변경될 때만 연산을 다시 수행하므로, 매 렌더링마다 filter 연산이 반복되는 것을 방지합니다.

🤔 '비용이 큰 연산'의 기준

모든 연산에 useMemo를 사용하는 것은 좋지 않습니다. 다음과 같은 기준으로 구분합니다.

사용 권장:

  • 수백 개 이상의 아이템을 filter, sort, map하는 경우
  • 복잡한 계산이나 데이터 가공이 포함된 경우 (예: 차트 데이터 변환)

지양:

  • 간단한 문자열 조합, 소규모 배열 등 연산이 매우 가벼운 경우
  • 단순 객체 생성 (useMemo(() => ({name, age}), [name, age])) 등 비교 비용이 더 클 때

💡 useRef와의 차이점: useRef는 변경되지 않는 값을 저장하기 위한 용도이고, useMemo는 특정 값의 계산 결과를 캐싱하는 용도입니다.


React.memo: 불필요한 리렌더링 방지 (컴포넌트)

React.memo는 컴포넌트를 인수로 받아, props가 변경되지 않으면 재렌더링을 방지하는 고차 컴포넌트(HOC)입니다.

const MemoizedComponent = memo(Component);
// 또는
export default memo(Component);

예를 들어, 입력창을 수정할 때 프로필, 시계 등 관련 없는 컴포넌트가 함께 리렌더링된다면 이는 불필요한 낭비입니다. React.memo는 이런 문제를 해결합니다.

예시

import { memo } from 'react';

const Profile = ({ user }) => {
  console.log('Profile 렌더링');
  return <div>{user.name}</div>;
};

export default memo(Profile);

부모 컴포넌트가 리렌더링되더라도, user prop이 바뀌지 않으면 Profile은 다시 렌더링되지 않습니다.

⚙️ 커스터마이즈된 비교 함수

React.memo는 얕은 비교(shallow compare)를 수행합니다. 객체 내부 속성까지 비교하려면 두 번째 인자로 커스터마이즈 함수를 전달할 수 있습니다.

export default memo(Component, (prev, next) => {
  return (
    prev.id === next.id &&
    prev.content === next.content &&
    prev.isDone === next.isDone
  );
});

⚠️ 주의점 및 사용 피해야 할 상황

React.memo는 props가 자주 바뀌는 경우, 비교 자체가 오히려 오버헤드가 될 수 있습니다.

💡 피해야 할 경우:

  • 실시간 데이터 그래프나 시계처럼 props가 계속 변하는 컴포넌트
  • 렌더링 비용이 매우 낮은 단순 UI 요소

또한 부모로부터 onClick={handleClick}과 같은 함수를 props로 받을 때, 부모 리렌더링 시 함수 참조가 새로 생성되어 memo 비교가 무의미해집니다. 이를 해결하기 위해 useCallback을 함께 사용합니다.


useCallback: 불필요한 함수 재생성 방지

useCallback함수 자체를 메모이제이션하는 훅으로, 매 렌더링마다 새 함수가 생성되는 문제를 방지합니다.

const onUpdate = useCallback((targetId) => {
  dispatch({ type: 'UPDATE', targetId });
}, []);
  • 첫 번째 인수: 메모이제이션할 콜백 함수
  • 두 번째 인수: 의존성 배열. 배열 내 값이 바뀔 때만 새 함수가 생성됩니다.

React.memo로 감싼 자식 컴포넌트에 함수를 props로 전달할 때 useCallback을 사용하면, props가 변경되지 않았음을 보장할 수 있습니다.

🚨 주의: Stale State (오래된 상태 참조)

function Counter() {
  const [count, setCount] = useState(0);

  const handleAlert = useCallback(() => {
    alert('현재 카운트: ' + count);
  }, []); // ❌ count가 빠져서 항상 0만 출력

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
      <button onClick={handleAlert}>카운트 확인</button>
    </div>
  );
}

이처럼 의존성 배열을 잘못 관리하면, 함수가 과거의 상태를 영원히 기억하는 버그가 발생합니다. 항상 훅 내부에서 사용하는 외부 변수는 의존성 배열에 정직하게 포함시켜야 합니다.

eslint-plugin-react-hooks를 사용하면 이러한 실수를 자동으로 탐지할 수 있습니다.


최적화 주의사항

  • 기능 구현 후 최적화: 먼저 동작하는 코드를 작성하고, 그 다음에 최적화를 적용하세요.
  • 무분별한 사용 금지: 최적화에도 비용이 존재합니다. 오히려 리렌더링하는 것이 더 빠를 수도 있습니다.
  • 적용 대상: 대량의 데이터, 복잡한 연산, 혹은 이벤트 핸들러가 많은 컴포넌트에만 사용하세요.

🧭 최종 정리: 최적화 적용 전략

상황적합한 훅 / 기능설명
값 계산이 무거움useMemo복잡한 연산의 결과값을 캐싱
함수가 자주 재생성됨useCallback동일한 함수를 메모이제이션
불필요한 리렌더링 발생React.memoprops가 바뀌지 않으면 재렌더링 방지

✅ 올바른 적용 순서

  1. 기능 구현 완료 후 최적화 고려

  2. React DevTools Profiler로 실제 병목 구간 확인

  3. 다음 순서로 적용:

    • Props 변경 없는 리렌더링 → React.memo
    • 부모에서 함수를 내려줌 → useCallback
    • 연산이 무거움 → useMemo

성능 최적화는 감(感)이 아니라 측정을 기반으로 해야 합니다. Profiler를 통해 실제로 렌더링이 지연되는 컴포넌트를 찾아 필요한 곳에만 정확히 적용하세요.


📚 참고 자료:
한 입 크기로 잘라먹는 리액트 (Inflearn)
React 공식 문서 - Performance Optimization

0개의 댓글