[React] React 성능 최적화를 위한 훅과 메모화 기법

찌끅·2024년 8월 26일

React 성능 최적화를 위한 훅과 메모화 기법

React 애플리케이션을 개발하다 보면 성능 최적화는 중요한 고려 사항 중 하나이다. 특히 규모가 커지거나 복잡한 상태 관리가 필요한 경우, 적절한 최적화 기법을 사용하여 애플리케이션의응답성을 향상시킬 수 있다.


useReducer를 통한 상태 관리 최적화

1. useReducer란?

useReducer는 React의 내장 훅으로, 복잡한 상태 로직을 보다 효율적으로 관리할 수 있도록 도와준다. Redux와 유사한 패턴을 따르며, 상태와 상태를 변경하는 디스패치 함수를 제공한다.

2. 왜 useReducer를 사용하는가?

  • 복잡한 상태로직 관리: 여러 개의 상태가 상호 연관되어 변경되어야 하는 경우 useReducer를 사용하면 코드의 가독성과 유지보수성이 향상된다.
  • 상태 변경 로직의 분리: 상태 변경 로직을 컴포넌트에서 분리하여 재사용성과 테스트 용이성을 높을 수 있다.
  • 성능 최적화: useReducer는 이전 상태와 액션에 따라 새로운 상태를 계산하기 때문에, 불필요한 상태 변경을 방지하고 성능을 향상시킬 수 있다.

3. 코드에서의 useReducer 사용 예시

import { useReducer, useRef } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'CREATE':
      return [action.data, ...state];
    case 'UPDATE':
      return state.map((item) =>
        item.id === action.targetId ? { ...item, isDone: !item.isDone } : item
      );
    case 'DELETE':
      return state.filter((item) => item.id !== action.targetId);
    default:
      return state;
  }
}

function App() {
  const [todos, dispatch] = useReducer(reducer, []);
  const idRef = useRef(3);
  
  // 예시 액션 디스패치
  const onCreate = (content) => {
    dispatch({
      type: 'CREATE',
      data: {
        id: idRef.current++,
        isDone: false,
        content,
        date: new Date().getTime(),
      },
    });
  };
  
  const onUpdate = (targetId) => {
    dispatch({
      type: 'UPDATE',
      targetId: targetId,
    });
  };
  
  const onDelete = (targetId) => {
    dispatch({
      type: 'DELETE',
      targetId: targetId,
    });
  };

  return (
    <div>
      {/* 컴포넌트 렌더링 */}
    </div>
  );
}

4. useReducer의 이점

  • 상태 변경의 예측 가능성: 모든 상태 변경이 명시적인 액션을 통해 이루어지므로 상태 변화가 예측 가능하다.
  • 코드 구조화: 상태 변경 로직이 하나의 함수로 모여 있어 코드 구조가 명확해진다
  • 성능 향상: 복잡한 상태 변경 로직을 효율적으로 처리하여 불필요한 렌더링을 방지한다.

useCallback을 통한 함수 재생성 방지

1. useCallback이란?

useCallback은 React의 내장 훅으로, 함수의 재생성을 방지하여 불필요한 렌더링을 줄이는 데 사용된다. 특정 의존성 배열이 변경되지 않는 한 동일한 함수를 반환한다.

2. 왜 useCallback을 사용하는가?

  • 성능 최적화: 함수가 불필요하게 재생성되는 것을 방지하여 하위 컴포넌트들이 불필요하게 재렌더링되는 것을 막는다.
  • 참조 동일성 유지: 함수의 참조가 변경되지 않도록 하여 React.memo와 함께 사용할 때 효과적이다.

3. 코드에서의 useCallback 사용 예시

import { useCallback, useRef } from 'react';

function App() {
  const [todos, dispatch] = useReducer(reducer, []);
  const idRef = useRef(3);

  const onCreate = useCallback((content) => {
    dispatch({
      type: 'CREATE',
      data: {
        id: idRef.current++,,
        isDone: false,
        content,
        date: new Date().getTime(),
      },
    });
  }, []);

  const onUpdate = useCallback((targetId) => {
    dispatch({
      type: 'UPDATE',
      targetId,
    });
  }, []);

  const onDelete = useCallback((targetId) => {
    dispatch({
      type: 'DELETE',
      targetId,
    });
  }, []);

  return (
    <div>
      {/* 컴포넌트 렌더링 */}
    </div>
  );
}

4. useCallback의 이점

  • 함수 재생성 방지: 렌더링마다 동일한 함수 참조를 유지하여 메모리 사용을 최적화한다.
  • 하위 컴포넌트 최적화: 함수 참조가 변경되지 않으므로, React.memo로 메모이제이션된 하위 컴포넌트들이 불필요하게 재렌더링되지 않는다.
  • 의존성 관리 용이: 의존성 배열을 통해 함수가 언제 재생성되어야 하는지 명확하게 관리할 수 있다.

useMemo를 통한 값 연산 최적화

1. useMemo란?

useMemo는 React의 내장훅으로, 값의 복잡한 계산 결과를 메모이제이션하여 불필요한 재계산을 방지한다. 특정 의존성 배열이 변경되지 않는 한 이전에 계산된 값을 반환한다.

2. 왜 useMemo를 사용하는가?

  • 비용이 큰 연산 최적화: 복잡하고 무거운 연산을 캐싱하여 성능을 향상시킨다.
  • 불필요한 재계산 방지: 동일한 입력에 대해 반복적으로 계산하지 않도록 한다.

3. 코드에서의 useMemo 사용 예시

import { useMemo } from 'react';

function App() {
  const [todos, dispatch] = useReducer(reducer, []);

  const completedTodos = useMemo(() => {
    return todos.filter(todo => todo.isDone);
  }, [todos]);

  return (
    <div>
      <h2>완료된 할 일: {completedTodos.length}</h2>
      {/* 나머지 컴포넌트 렌더링 */}
    </div>
  );
}

4. useMemo의 이점

  • 연산 비용 절감: 동일한 입력에 대해 재계산을 피함으로써 애플리케이션의 성능을 향상시킨다.
  • 렌더링 최적화: 값의 변경이 없을 경우 컴포넌트의 재렌더링을 방지하거나 최소화할 수 있다.
  • 코드 가독성 향상: 복잡한 계산 로직을 분리하여 코드의 가독성과 유지보수성을 높인다.

React.memo를 통한 컴포넌트 재렌더링 방지

1. React.memo란?

React.memo는 고차 컴포넌트로, 컴포넌트를 메모이제이션하여 동일한 props가 전달될 경우 이전에 렌더링된 결과를 재사용한다.

2. 왜 React.memo를 사용하는가?

  • 불필요한 재렌더링 방지: 부모 컴포넌트가 렌더링되더라도 props가 변경되지 않은 경우 하위 컴포넌트의 재렌더링을 방지한다.
  • 성능 최적화: 특히 큰 규모의 컴포넌트 트리에서 불필요한 렌더링을 줄여 전체적인 성능을 향상시킨다.

3. 코드에서의 React.memo 사용 예시

import { memo } from 'react';
import './TodoItem.css';

const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
  const onChangeCheckbox = () => {
    onUpdate(id);
  };

  const onClickDeleteButton = () => {
    onDelete(id);
  };

  return (
    <div className="TodoItem">
      <input
        onChange={onChangeCheckbox}
        readOnly
        checked={isDone}
        type="checkbox"
      />
      <div className="content">{content}</div>
      <div className="date">{new Date(date).toLocaleDateString()}</div>
      <button onClick={onClickDeleteButton}>삭제</button>
    </div>
  );
};

// export default memo(TodoItem, (prevProps, nextProps) => {
//   // 반환 값에 따라, Props가 바뀌었는지 안 바뀌었는지 판단
//   // T -> Props 바뀌지 않음 -> 리렌더링 X
//   // F -> Props 바뀜 -> 리렌더링 O

//   if (prevProps.id !== nextProps.id) {
//     return false;
//   }
//   if (prevProps.isDone !== nextProps.isDone) {
//     return false;
//   }
//   if (prevProps.content !== nextProps.content) {
//     return false;
//   }
//   if (prevProps.date !== nextProps.date) {
//     return false;
//   }

//   return true;
// });

export default memo(TodoItem);

4. React.memo의 이점

  • 렌더링 성능 향상: 불필요한 렌더링을 방지하여 UI 업데이트 성능을 향상시킨다.
  • 쉬운 적용: 컴포넌트를 간단하게 감싸는 것만으로도 효과적인 최적화를 이룰 수 있다.
  • 맞춤 비교 함수 지원: 두 번째 인자로 비교 함수를 전달하여 보다 세밀한 렌더링 제어가 가능하다.
export default memo(TodoItem, (prevProps, nextProps) => {
  return prevProps.isDone === nextProps.isDone && prevProps.content === nextProps.content;
});

언제 최적화를 적용해야 할까?

1. 모든 경우에 최적화가 필요한가?

최적화는 항상 이점을 주는 것은 아니다. 오히려 지나친 최적화는 코드의 복잡성을 높이고 유지보수를 어렵게 만들 수 있다. 따라서 최적화는 필요한 경우에만, 그리고 그 필요성이 명확할 때 적용하는 것이 중요하다.

2. 최적화가 필요한 상황

  • 렌더링 성능 저하: 대량의 데이터 처리나 복잡한 연산으로 인해 UI 업데이트가 느려지는 경우
  • 불필요한 재렌더링 감지: 개발자 도구나 로깅을 통해 컴포넌트가 불필요하게 자주 재렌더링되는 것을 발견한 경우
  • 사용자 경험 저하: 애플리케이션의 응답성이 떨어져 사용자 경험이 저하되는 경우
  • 모바일 및 저사양 디바이스 지원: 제한된 자원에서 애플리케이션이 원활하게 동작하도록 최적화가 필요한 경우

3. 최적화를 적용할 때 고려 사항

  • 복잡성 vs 성능: 최적화로 인해 코드 복잡성이 크게 증가한다면 그에 따른 이득이 충분한지 평가해야 한다.
  • 측정 및 분석: 최적화를 적용하기 전에 실제로 문제가 있는지를 측정하고, 적용 후에도 성능 개선이 이루어졌는지 확인해야 한다.
  • 유지보수성: 최적화된 코드가 향후 유지보수와 확장에 어떤 영향을 미칠지 고려해야 한다.

4. 단계적인 접근

  1. 문제 식별: 성능 문제가 실제로 존재하는지 확인한다.
  2. 원인 분석: 문제가 발생하는 원인을 정확히 파악한다.
  3. 적용 가능한 최적화 선택: 상황에 맞는 최적화 기법을 선택한다.
  4. 적용 및 테스트: 최적화를 적용하고 충분한 테스트를 통해 정상 동작과 성능 개선을 확인한다.
  5. 모니터링 및 조정: 애플리케이션을 지속적으로 모니터링하며 필요한 경우 추가 조정을 한다.

결론

React에서으 성능 최적화는 사용자 경험을 향상시키고 애플리케이션의 효율성을 높이는 중요한 요소이다. useReducer, useCallback, useMemo, React.memo와 같은 훅과 기법들은 이러한 최적화를 달성하는 데 효과적인 도구들이다.

  • useReducer를 통해 복잡한 상태 관리를 명확하고 효율적으로 처리할 수 있다.
  • useCallbackuseMemo는 함수오 값의 재생성을 방지하여 불필요한 연산과 렌더링을 최소화한다.
  • React.memo는 컴포넌트의 불필요한 재렌더링을 방지하여 전체적인 렌더링 성능을 향상시킨다.

그러나 이러한 최적화 기법들은필요에 따라 신중하게 적용되어야 하며, 과도한 최적화는 오히려 코드의 복잡성을 증가시키고 유지보수를 어렵게 만들 수 있다. 따라서 성능 문제를 명확히 식별하고, 그에 맞는 최적화 전략을 단계적으로 적용하는 것이 중요하다.

0개의 댓글