본문 바로가기

자기계발

리액트 성능 최적화의 기초: useMemo, useCallback, memo

1. React 클래스형 컴포넌트 생명주기 메서드

클래스형 컴포넌트는 componentDidMount, componentDidUpdate, componentWillUnmount와 같은 생명주기 메서드를 사용하여 컴포넌트의 특정 시점에 원하는 작업을 수행할 수 있습니다.

 

componentDidMount

 

  • 실행 시점: 컴포넌트가 처음으로 DOM에 삽입된 직후
  • 용도: 초기 데이터 로드, 이벤트 리스너 등록, DOM 조작 등 초기화 작업
componentDidMount() {
  console.log('컴포넌트가 마운트되었습니다.');
}

 

 

 

componentDidUpdate

 

  • 실행 시점: 컴포넌트가 업데이트된 직후
  • 용도: 이전과 현재의 상태나 props를 비교하여 추가 작업을 수행할 때 사용
componentDidUpdate(prevProps, prevState) {
  if (prevProps.value !== this.props.value) {
    console.log('컴포넌트가 업데이트되었습니다.');
  }
}

 

 

 

componentWillUnmount

 

  • 실행 시점: 컴포넌트가 DOM에서 제거되기 직전.
  • 용도: 타이머 제거, 이벤트 리스너 해제, 데이터 구독 해제 등 정리 작업.

 

componentWillUnmount() {
  console.log('컴포넌트가 언마운트됩니다.');
}

 

 

2. 함수형 컴포넌트의 생명주기

함수형 컴포넌트는 생명주기 메서드 대신 useEffect 훅을 사용하여 컴포넌트의 특정 시점에 작업을 수행합니다.

useEffect: componentDidMount, componentDidUpdate, componentWillUnmount의 역할을 모두 수행할 수 있는 다목적 훅입니다.

  • 마운트 시 한 번만 실행: 빈 의존성 배열을 사용하여 마운트 시점에만 실행됩니다.
useEffect(() => {
  console.log('컴포넌트가 마운트되었습니다.');
}, []);
  • 업데이트 시마다 실행: 의존성 배열이 없으면 모든 렌더링 시점에 실행됩니다.
useEffect(() => {
  console.log('컴포넌트가 업데이트되었습니다.');
});
  • 언마운트 시 클린업: 리턴 함수에서 정리 작업을 수행합니다.
useEffect(() => {
  return () => {
    console.log('컴포넌트가 언마운트됩니다.');
  };
}, []);

 

 

 

3. 함수형 컴포넌트의 최적화 방법

React에서는 불필요한 렌더링과 성능 문제를 해결하기 위해 useMemo, useCallback, React.memo 같은 최적화 도구를 제공합니다.

 

useMemo: 계산량이 많은 함수의 결과를 메모이제이션하여, 불필요한 재계산을 방지합니다.

  • 사용 시점: 값이 변경되지 않으면 이전의 계산 결과를 재사용하고 싶을 때
const memoizedValue = useMemo(() => expensiveFunction(input), [input]);

 

 

useCallback: 함수 인스턴스를 메모이제이션하여, 컴포넌트가 다시 렌더링될 때마다 새로운 함수가 생성되지 않도록 합니다.

  • 사용 시점: 함수의 참조가 변하지 않도록 유지하고 싶을 때
const handleClick = useCallback(() => {
  console.log('버튼 클릭!');
}, []);

 

 

React.memo: 컴포넌트를 메모이제이션하여, props가 변경되지 않는 한 불필요한 재렌더링을 방지합니다.

  • 사용 시점: 컴포넌트가 동일한 props로 불필요하게 다시 렌더링될 때
const MyComponent = React.memo(({ value }) => {
  return <div>{value}</div>;
});

 

 

 

useMemo, useCallback, memo를 활용할 수 있는 과제:

import React, { useState } from 'react';
import './App.css';
/* 
1. **최적화 적용**:
    1. 주어진 기본 코드를 `useCallback`, `useMemo`, `React.memo`를 활용하여 최적화합니다.
    2. `ListItem` 컴포넌트를 `React.memo`로 래핑하여, 컴포넌트가 불필요하게 재렌더링되지 않도록 합니다.
    3. `items` 배열을 `useMemo`를 사용하여 메모이제이션하고, `filteredItems` 배열도 `useMemo`를 사용하여 메모이제이션합니다.
    4. `handleItemClick` 함수를 `useCallback`으로 메모이제이션하여, 함수 인스턴스가 불필요하게 새로 생성되지 않도록 합니다.
2. **변경 사항 확인**:
    1. 최적화 적용 후, 브라우저의 콘솔을 확인하여 `ListItem` 컴포넌트의 렌더링 로그를 통해 최적화가 올바르게 적용되었는지 확인합니다.
*/
// 리스트 항목 컴포넌트
const ListItem = ({ item, onClick }) => {
  console.log(`Rendering ${item}`);
  return <li onClick={() => onClick(item)}>{item}</li>;
};

const App = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedItem, setSelectedItem] = useState(null);

  const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Fig', 'Grape'];

  const filteredItems = items.filter((item) =>
    item.toLowerCase().includes(searchTerm.toLowerCase())
  );

  const handleItemClick = (item) => {
    setSelectedItem(item);
  };

  return (
    <div className="app-wrapper">
      <div className="app-container">
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Search..."
          className="search-input"
        />
        <ul className="item-list">
          {filteredItems.map((item) => (
            <ListItem key={item} item={item} onClick={handleItemClick} />
          ))}
        </ul>
        {selectedItem && (
          <p className="selected-item">Selected Item: {selectedItem}</p>
        )}
      </div>
    </div>
  );
};

export default App;

 

과제 정답 코드:

import React, { memo, useCallback, useMemo, useState } from 'react';
import './App.css';
/* 
1. **최적화 적용**:
    1. 주어진 기본 코드를 `useCallback`, `useMemo`, `React.memo`를 활용하여 최적화합니다.
    2. `ListItem` 컴포넌트를 `React.memo`로 래핑하여, 컴포넌트가 불필요하게 재렌더링되지 않도록 합니다.
    3. `items` 배열을 `useMemo`를 사용하여 메모이제이션하고, `filteredItems` 배열도 `useMemo`를 사용하여 메모이제이션합니다.
    4. `handleItemClick` 함수를 `useCallback`으로 메모이제이션하여, 함수 인스턴스가 불필요하게 새로 생성되지 않도록 합니다.
2. **변경 사항 확인**:
    1. 최적화 적용 후, 브라우저의 콘솔을 확인하여 `ListItem` 컴포넌트의 렌더링 로그를 통해 최적화가 올바르게 적용되었는지 확인합니다.
*/
// 리스트 항목 컴포넌트

const ListItem = memo(({ item, onClick }) => {
  console.log(`Rendering ${item}`);
  return <li onClick={() => onClick(item)}>{item}</li>;
});

const App = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedItem, setSelectedItem] = useState(null);

  const items = useMemo(() => {
    return ['Apple', 'Banana', 'Cherry', 'Date', 'Fig', 'Grape'];
  }, []);

  const filteredItems = useMemo(
    () =>
      items.filter((item) =>
        item.toLowerCase().includes(searchTerm.toLowerCase())
      ),
    [items, searchTerm]
  );

  const handleItemClick = useCallback((item) => {
    setSelectedItem(item);
  }, []);

  return (
    <div className="app-wrapper">
      <div className="app-container">
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Search..."
          className="search-input"
        />
        <ul className="item-list">
          {filteredItems.map((item) => (
            <ListItem key={item} item={item} onClick={handleItemClick} />
          ))}
        </ul>
        {selectedItem && (
          <p className="selected-item">Selected Item: {selectedItem}</p>
        )}
      </div>
    </div>
  );
};

export default App;

 

useMemo vs React.memo 차이점

  • useMemo:
    • 기능: 값을 메모이제이션하여 재계산을 피하는 훅입니다. 의존성 배열의 값이 변경되지 않는 한 이전에 계산된 값을 반환합니다.
    • 사용 목적: 복잡한 계산 결과를 메모이제이션하거나 값이 변경되지 않는 한 불필요한 재계산을 방지합니다. 예를 들어, 리스트 필터링, 복잡한 객체 생성 등.
    • 적용 대상: 계산된 값이나 함수의 반환값.
  • React.memo:
    • 기능: 컴포넌트를 메모이제이션하여, props가 변경되지 않는 한 컴포넌트를 재렌더링하지 않습니다.
    • 사용 목적: 컴포넌트의 불필요한 재렌더링을 방지하여 성능을 최적화합니다.
    • 적용 대상: React 컴포넌트.

요약:

  • useMemo는 값을 메모이제이션하여 재계산을 방지하고,
  • React.memo는 컴포넌트를 메모이제이션하여 불필요한 재렌더링을 방지합니다.

위 최적화를 적용하면 성능 개선이 이루어지며, 브라우저 콘솔에서 ListItem의 렌더링이 필요할 때만 발생하는지 확인해 최적화가 올바르게 적용되었는지 점검할 수 있습니다.

 

 

 

const filteredItems = useMemo(
  () => items.filter((item) => item.toLowerCase().includes(searchTerm.toLowerCase())),
  [items, searchTerm]
);

이 코드에서 useMemo의 두 번째 인자인 [items, searchTerm]은 의존성 배열입니다. 이 배열이 중요한 이유는 useMemo가 언제 콜백 함수를 실행해야 하는지를 결정하기 때문입니다.

이유:

  • items: items 배열이 변경되면 filteredItems를 다시 계산해야 하기 때문에 의존성 배열에 포함합니다. 이 배열이 변경되지 않으면 filteredItems도 변경될 필요가 없으므로 재계산하지 않습니다.
  • searchTerm: 사용자가 검색어를 입력할 때마다 searchTerm이 변경되며, 이에 따라 filteredItems도 달라져야 합니다. 의존성 배열에 searchTerm을 포함함으로써 사용자의 입력에 따라 필터링된 결과가 최신 상태로 유지됩니다.

의존성 배열의 역할:

  • 의존성 배열의 값이 변경될 때만 useMemo의 콜백 함수가 다시 실행됩니다.
  • 최적화 목적: 불필요한 재계산을 방지하고, items와 searchTerm이 동일하면 이전에 메모이제이션한 값을 재사용함으로써 성능을 개선합니다.

따라서 [items, searchTerm]를 의존성으로 설정하는 것은 filteredItems가 정확하게 필요한 시점에만 계산되도록 하여, 성능과 동작의 일관성을 유지하게 합니다.