on my way

한 입 크기로 잘라먹는 리액트 8장: 최적화 본문

Computer Science/React

한 입 크기로 잘라먹는 리액트 8장: 최적화

wingbeat 2024. 7. 16. 14:40

 

메모이제이션과 useMemo

최적화란?

최적화는 웹 서비스의 성능을 개선하는 기술을 의미합니다. 최적화는 사용자가 웹 서비스를 사용할 때 불필요하게 기다리지 않도록 하여 사용자 경험을 개선하는 데 중요한 역할을 합니다.

 

최적화는 웹 애플리케이션의 성능을 개선하기 위해 중요하지만, 모든 것을 최적화하는 것은 반드시 좋은 것만은 아닙니다. 최적화를 다 하는 것에 대해 고려할 몇 가지 중요한 점들을 알아봅시다.

최적화의 이점

  1. 성능 향상:
    • 불필요한 연산과 리렌더링을 줄여 애플리케이션의 반응성을 높입니다.
    • 사용자 경험을 향상시켜 애플리케이션이 더 빠르고 부드럽게 동작하게 만듭니다.
  2. 리소스 절약:
    • 서버와 클라이언트의 리소스를 절약할 수 있습니다.
    • 배터리 수명, 데이터 사용량 등의 측면에서 모바일 사용자를 위한 최적화도 가능합니다.

최적화의 단점

  1. 복잡성 증가:
    • 코드가 복잡해지고 유지보수하기 어려워질 수 있습니다.
    • 최적화된 코드가 직관적이지 않을 수 있어, 새로운 개발자나 팀원이 이해하기 어려울 수 있습니다.
  2. 디버깅 어려움:
    • 최적화 과정에서 발생하는 버그를 추적하고 해결하는 것이 더 어려울 수 있습니다.
  3. 개발 시간 증가:
    • 최적화 작업은 추가적인 시간과 노력이 필요합니다.
    • 초기 개발 단계에서는 기능 구현이 더 중요할 수 있습니다.

최적화 시 고려 사항

  1. 우선순위 정하기:
    • 성능 문제가 실제로 발생하는 부분을 식별하고, 그 부분에 집중적으로 최적화합니다.
    • 사용자에게 직접적인 영향을 미치는 중요한 기능이나 페이지를 먼저 최적화합니다.
  2. 점진적 최적화:
    • 처음부터 모든 것을 최적화하기보다는, 문제를 발견할 때마다 점진적으로 최적화합니다.
    • 새로운 기능을 추가하면서 필요에 따라 최적화 작업을 병행합니다.
  3. 유의미한 최적화:
    • 성능에 큰 영향을 미치는 부분에 대해 최적화합니다.
    • 작은 성능 개선을 위해 과도한 최적화를 시도하는 것은 피합니다.

 

메모이제이션

메모이제이션은 특정 입력에 대한 결과를 기억해 두었다가 동일한 요청이 들어오면 저장된 결과를 반환하는 방법입니다. 이 기술은 불필요한 연산을 줄여 프로그램의 실행 속도를 높이는 데 매우 유용합니다.

예를 들어, 계산이 복잡한 함수가 있다고 가정해 보겠습니다. 이 함수를 매번 호출할 때마다 동일한 계산을 수행하는 대신, 한 번 계산한 결과를 저장해 두고 동일한 입력이 들어오면 저장된 결과를 반환하면 시간과 자원을 절약할 수 있습니다. 이를 메모이제이션이라고 합니다.

const expensiveCalculation = (num) => {
  console.log('계산 중...');
  return num * num;
};

const memoizedCalculation = memoize(expensiveCalculation);

console.log(memoizedCalculation(5)); // 계산 중... 25
console.log(memoizedCalculation(5)); // 25 (계산 생략)

위 예제에서 memoize 함수는 주어진 함수의 결과를 캐싱하여 동일한 입력에 대해 불필요한 재계산을 방지합니다.

useMemo

useMemo는 리액트 훅으로, 메모이제이션을 이용해 연산의 결과를 기억하고 필요할 때 재사용하여 불필요한 함수 호출을 막아줍니다. useMemo를 사용하면 컴포넌트의 성능을 최적화할 수 있습니다.

 

useMemo의 기본 사용법은 다음과 같습니다:

const memoizedValue = useMemo(() => {
  // 복잡한 계산 또는 연산
  return computeExpensiveValue(a, b);
}, [a, b]);

여기서 useMemo는 두 번째 인수로 전달된 의존성 배열 [a, b]의 값이 변경될 때만 첫 번째 인수로 전달된 함수를 다시 실행합니다. 값이 변경되지 않으면 이전에 계산된 값을 재사용합니다.

 

예제: 할 일 관리 앱에서 useMemo 사용하기

할 일 관리 앱에서 불필요한 함수 호출을 방지하기 위해 useMemo를 사용하는 예제를 살펴보겠습니다.

먼저, 할 일 목록을 분석하는 analyzeTodo 함수를 useMemo로 최적화합니다:

 

예제: 할 일 관리 앱에서 불필요한 함수 호출 방지하기

import { useMemo } from "react";

const TodoList = ({ todo }) => {
  const analyzeTodo = useMemo(() => {
    const totalCount = todo.length;
    const doneCount = todo.filter(it => it.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return { totalCount, doneCount, notDoneCount };
  }, [todo]);

  return (
    <div>
      <h4>Todo List</h4>
      <div>총 개수: {analyzeTodo.totalCount}</div>
      <div>완료된 할 일: {analyzeTodo.doneCount}</div>
      <div>미완료된 할 일: {analyzeTodo.notDoneCount}</div>
    </div>
  );
};

이 예제에서는 useMemo를 사용하여 analyzeTodo 함수가 불필요하게 호출되지 않도록 했습니다. todo 배열이 변경될 때만 analyzeTodo 함수가 다시 실행되며, 그렇지 않으면 이전 결과를 재사용합니다.

요약

  • 최적화는 웹 서비스의 성능을 개선하는 기술입니다.
  • 메모이제이션은 동일한 입력에 대해 이전에 계산한 결과를 재사용하여 불필요한 연산을 줄이는 방법입니다.
  • useMemo는 리액트 훅으로, 메모이제이션을 이용해 연산의 결과를 기억하고 필요할 때 재사용하여 컴포넌트의 성능을 최적화합니다.

이와 같이, 메모이제이션과 useMemo를 사용하면 리액트 애플리케이션의 성능을 향상시킬 수 있습니다.


고차 컴포넌트 (Higher Order Component, HOC)와 횡단 관심사 (Cross-Cutting Concerns)

고차 컴포넌트 (HOC)

고차 컴포넌트는 리액트에서 컴포넌트 로직을 재사용하기 위한 고급 기술입니다. 고차 컴포넌트는 함수로, 다른 컴포넌트를 인수로 받아들이고, 새로운 컴포넌트를 반환합니다. 이때 반환되는 컴포넌트는 인수로 받은 컴포넌트를 감싸거나 기능을 추가한 '강화된 컴포넌트'입니다.

// 고차 컴포넌트의 기본 형태
const withEnhancement = (WrappedComponent) => {
  return class extends React.Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

고차 컴포넌트를 사용하는 이유

고차 컴포넌트를 사용하면 다음과 같은 장점이 있습니다:

  1. 로직 재사용: 여러 컴포넌트에서 공통으로 사용하는 로직을 고차 컴포넌트로 분리하여 코드 중복을 줄일 수 있습니다.
  2. 관심사의 분리: 컴포넌트의 주요 비즈니스 로직과 부가적인 로직(횡단 관심사)을 분리하여 코드의 가독성과 유지보수성을 높일 수 있습니다.

예제: 로깅 기능 추가하기

// 로깅 기능을 추가하는 고차 컴포넌트
const withLogging = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      console.log(`${WrappedComponent.name} 컴포넌트가 마운트되었습니다.`);
    }
    componentWillUnmount() {
      console.log(`${WrappedComponent.name} 컴포넌트가 언마운트되었습니다.`);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

// 사용할 컴포넌트
const MyComponent = () => {
  return <div>안녕하세요!</div>;
};

// 고차 컴포넌트를 사용하여 강화된 컴포넌트 생성
const EnhancedComponent = withLogging(MyComponent);

// 사용할 때는 강화된 컴포넌트를 사용
<EnhancedComponent />;

이 예제에서는 withLogging이라는 고차 컴포넌트를 사용하여 MyComponent가 마운트되고 언마운트될 때 콘솔에 로그를 출력하는 기능을 추가했습니다.

횡단 관심사 (Cross-Cutting Concerns)

횡단 관심사는 여러 모듈이나 컴포넌트에 걸쳐 공통으로 사용되는 로직이나 기능을 의미합니다. 예를 들어, 로깅, 인증, 데이터 접근, 에러 처리 등이 횡단 관심사에 해당합니다.

횡단 관심사의 문제

횡단 관심사는 여러 곳에서 동일한 코드를 반복해서 작성하게 만들기 때문에 코드 중복이 발생할 수 있습니다. 이는 코드의 가독성을 떨어뜨리고, 유지보수를 어렵게 만듭니다.

고차 컴포넌트를 이용한 횡단 관심사 해결

고차 컴포넌트를 이용하면 횡단 관심사를 효율적으로 처리할 수 있습니다. 예를 들어, 여러 컴포넌트에서 공통으로 사용되는 로깅 기능을 고차 컴포넌트로 분리하면, 코드 중복을 줄이고, 필요할 때마다 고차 컴포넌트를 적용하여 해당 기능을 추가할 수 있습니다.

예제: 고차 컴포넌트를 이용한 횡단 관심사 해결

// 로깅 기능을 추가하는 고차 컴포넌트
const withLogging = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      console.log(`${WrappedComponent.name} 컴포넌트가 마운트되었습니다.`);
    }
    componentWillUnmount() {
      console.log(`${WrappedComponent.name} 컴포넌트가 언마운트되었습니다.`);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

// 사용할 컴포넌트
const ComponentA = () => {
  return <div>컴포넌트 A</div>;
};

const ComponentB = () => {
  return <div>컴포넌트 B</div>;
};

// 고차 컴포넌트를 사용하여 강화된 컴포넌트 생성
const EnhancedComponentA = withLogging(ComponentA);
const EnhancedComponentB = withLogging(ComponentB);

// 사용할 때는 강화된 컴포넌트를 사용
<EnhancedComponentA />;
<EnhancedComponentB />;

이 예제에서 ComponentA와 ComponentB는 고차 컴포넌트 withLogging을 통해 강화되어 마운트와 언마운트 시에 로그를 출력합니다. 이를 통해 횡단 관심사인 로깅 기능을 효율적으로 처리할 수 있습니다.

요약

  • 고차 컴포넌트 (HOC): 다른 컴포넌트를 인수로 받아 새로운 컴포넌트를 반환하는 함수입니다. 로직 재사용과 관심사의 분리를 돕습니다.
  • 횡단 관심사 (Cross-Cutting Concerns): 여러 컴포넌트에서 공통으로 사용되는 로직이나 기능을 의미합니다. 로깅, 인증, 에러 처리 등이 이에 해당합니다.
  • 고차 컴포넌트를 이용한 해결: 고차 컴포넌트를 사용하여 횡단 관심사를 처리하면 코드 중복을 줄이고, 코드의 가독성과 유지보수성을 높일 수 있습니다.

고차 컴포넌트와 횡단 관심사를 이해하고 활용하면, 리액트 애플리케이션의 코드 구조를 더 효율적이고 관리하기 쉽게 만들 수 있습니다.


불필요한 컴포넌트 리렌더링 방지하기

React.memo

React.memo는 컴포넌트를 메모이제이션하여, 전달된 props가 변경되지 않는 한 컴포넌트가 리렌더링되지 않도록 하는 기능입니다. 이는 컴포넌트의 성능을 최적화하는 데 유용합니다.

예제: Header 컴포넌트 리렌더 방지하기

아래 예제에서는 Header 컴포넌트를 React.memo를 사용하여 메모이제이션합니다. 이로써 Header 컴포넌트는 props가 변경되지 않는 한 다시 렌더링되지 않습니다.

import React from "react";

const Header = () => {
  console.log("Header 업데이트");
  return <h1>오늘은 {new Date().toLocaleDateString()}입니다</h1>;
};

export default React.memo(Header);

위 코드를 통해 Header 컴포넌트가 업데이트될 때마다 콘솔에 "Header 업데이트" 메시지가 출력됩니다. 그러나 React.memo를 사용하면 props가 변경되지 않는 한 이 메시지는 한 번만 출력됩니다.

 

함수 재생성 방지하기

useCallback

useCallback은 컴포넌트가 리렌더링될 때 함수가 다시 생성되지 않도록 메모이제이션하는 리액트 훅입니다.

이는 불필요한 함수 재생성을 방지하고 성능을 최적화합니다.

 

예제: 할 일 관리 앱에서 함수 재생성 방지하기

아래 예제는 useCallback을 사용하여 할 일 관리 앱에서 함수 재생성을 방지하는 방법을 보여줍니다.

import { useCallback, useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, action.payload];
    case "TOGGLE_TODO":
      return state.map(todo => 
        todo.id === action.payload ? { ...todo, isDone: !todo.isDone } : todo
      );
    case "DELETE_TODO":
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
};

const App = () => {
  const [todo, dispatch] = useReducer(reducer, []);

  const addTodo = useCallback(content => {
    dispatch({ type: "ADD_TODO", payload: { id: Date.now(), content, isDone: false } });
  }, []);

  const toggleTodo = useCallback(id => {
    dispatch({ type: "TOGGLE_TODO", payload: id });
  }, []);

  const deleteTodo = useCallback(id => {
    dispatch({ type: "DELETE_TODO", payload: id });
  }, []);

  return (
    <div>
      <Header />
      <TodoList todo={todo} onToggle={toggleTodo} onDelete={deleteTodo} />
    </div>
  );
};

export default App;

위 코드에서 useCallback을 사용하여 addTodo, toggleTodo, deleteTodo 함수를 메모이제이션합니다.

이는 App 컴포넌트가 리렌더링될 때마다 이러한 함수들이 재생성되는 것을 방지합니다.

각 함수는 해당 의존성 배열(예: 빈 배열 []의 경우 처음 한 번만 생성되고 다시 생성되지 않음)이 변경되지 않는 한 메모이제이션된 함수를 반환합니다.

  • addTodo 함수는 새로운 할 일을 추가합니다. useCallback을 사용하여 처음 한 번만 생성됩니다.
  • toggleTodo 함수는 할 일의 완료 상태를 토글합니다. useCallback을 사용하여 처음 한 번만 생성됩니다.
  • deleteTodo 함수는 할 일을 삭제합니다. useCallback을 사용하여 처음 한 번만 생성됩니다.

이러한 최적화를 통해 불필요한 리렌더링과 함수 재생성을 방지하고 성능을 개선할 수 있습니다.

 

최적화는 웹 애플리케이션의 성능을 향상시키는 중요한 작업이지만, 모든 것을 최적화하는 것은 불필요할 수 있습니다. 성능 개선이 필요한 부분을 식별하고, 점진적이고 유의미한 최적화를 진행하는 것이 좋습니다. 최적화로 인한 복잡성 증가와 디버깅의 어려움을 고려하여, 필요할 때마다 최적화를 적용하는 것이 바람직합니다.


결론

  • 최적화는 웹 서비스 성능을 개선하는 중요한 기술입니다.
  • 메모이제이션을 통해 불필요한 연산을 줄이고 성능을 높일 수 있습니다.
  • useMemoReact.memo를 사용하여 불필요한 함수 호출과 컴포넌트 리렌더링을 방지합니다.
  • useCallback을 사용하여 컴포넌트가 리렌더링될 때 함수가 다시 생성되지 않도록 합니다.

이러한 최적화 기법들을 통해 리액트 애플리케이션의 성능을 개선할 수 있습니다. 최적화는 항상 마지막에 하는 것이 좋으며, 모든 것을 최적화할 필요는 없습니다. 필요에 따라 적절한 최적화 기법을 적용하여 성능을 개선하세요.