리액트 up 8. 리액트의 최적화 원리

Posted by : on

Category : react-up


최적화란

리액트에서 최적화란 불필요한 연산이나 렌더링을 방지하여, 애플리케이션의 성능을 개선하는 것을 말한다.

useMemo,React.memo,useCallback

리액트 18이상의 버전부터는 개선된 렌더링 전략이 사용되므로 위 훅의 필요성이 예전보다 많이 줄어들었다. 그래서 꼭 필요한 경우 아니면 사용할 필요가 없다.

useMemo

복잡한 계산 결과를 메모이제이션(캐싱)하여 불필요한 재계산을 방지하는 훅이다.

import React, { useState, useMemo } from "react";

function ExpensiveCalculation() {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(0);

  // 복잡한 계산 - count가 변경될 때만 재계산됨
  const expensiveResult = useMemo(() => {
    console.log("복잡한 계산 실행!");
    let result = 0;
    for (let i = 0; i < 1000000000; i++) {
      result += count;
    }
    return result;
  }, [count]); // count가 변경될 때만 재계산

  return (
    <div>
      <p>Count: {count}</p>
      <p>계산 결과: {expensiveResult}</p>
      <button onClick={() => setCount(count + 1)}>Count 증가</button>
      <button onClick={() => setOtherState(otherState + 1)}>
        다른 상태 변경 (계산 안함)
      </button>
    </div>
  );
}

언제 써야할까? 계산 비용이 정말 큰 연산이 있을 때만 사용해라. 대부분의 경우 그냥 일반 변수로 충분하다.

React.memo

부모의 리렌더링으로 인하여 자식까지 리렌더링되는 그 불필요함을 방지 컴포넌트를 메모이제이션하여 props가 변경되지 않으면 리렌더링을 방지한다.

import React, { useState, memo } from "react";

// 자식 컴포넌트 - React.memo로 감싸서 최적화
const ExpensiveComponent = memo(({ name }) => {
  console.log("ExpensiveComponent 렌더링:", name);

  // 무거운 렌더링을 시뮬레이션
  let startTime = performance.now();
  while (performance.now() - startTime < 100) {
    // 100ms 동안 아무것도 하지 않음 (렌더링이 무거운 것처럼)
  }

  return <div>이름: {name}</div>;
});

// 부모 컴포넌트
function ParentComponent() {
  const [name, setName] = useState("철수");
  const [counter, setCounter] = useState(0);

  return (
    <div>
      <button onClick={() => setName(name === "철수" ? "영희" : "철수")}>
        이름 변경
      </button>
      <button onClick={() => setCounter(counter + 1)}>
        카운터 증가 (자식은 리렌더링 안됨)
      </button>
      <p>카운터: {counter}</p>
      <ExpensiveComponent name={name} />
    </div>
  );
}

언제 써야할까? 정말 렌더링 비용이 큰 컴포넌트가 불필요하게 자주 리렌더링될 때만 사용해라.

useCallback

React.memo로 최적화된 프롭스라고해도 함수를 프롭스로 넘기면 리렌더링 된다. 그떄 그 함수를 메모이제이션하여 불필요한 함수 재생성을 방지한다.

import React, { useState, useCallback } from "react";

function SearchComponent() {
  const [query, setQuery] = useState("");
  const [searchHistory, setSearchHistory] = useState([]);

  // useCallback으로 함수 메모이제이션
  // 의존성 배열이 변경되지 않는 한 같은 함수 인스턴스 유지
  const handleSearch = useCallback(() => {
    console.log("검색 실행:", query);
    setSearchHistory((prev) => [...prev, query]);
    // API 호출 등의 로직...
  }, [query]); // query가 변경될 때만 함수 재생성

  // 이벤트 핸들러도 메모이제이션 가능
  const handleInputChange = useCallback((e) => {
    setQuery(e.target.value);
  }, []); // 의존성 없음 - 컴포넌트 생명주기 동안 같은 함수 유지

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleInputChange}
        placeholder="검색어 입력"
      />
      <button onClick={handleSearch}>검색</button>
      <div>
        <h3>검색 기록:</h3>
        <ul>
          {searchHistory.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

언제 써야할까? 주로 자식 컴포넌트에 함수를 props로 전달할 때, 특히 그 자식이 React.memo로 최적화되어 있을 때만 필요하다. 아니면 의존성 배열에 함수를 넣어야 할 때도 사용한다.

결론

리액트 18에서는 자동 배칭(Automatic Batching)과 같은 개선된 렌더링 전략 덕분에 이러한 최적화의 필요성이 줄었다. 성능 문제가 실제로 측정되기 전까지는 과도한 최적화를 피하는 것이 좋다. 코드가 복잡해지고 가독성이 떨어질 수 있기 때문이다. 정말 필요한 경우에만 사용하자!


About 유재석
유재석

개발자 유재석 입니다. Web Developer.

Email : jaeseok9405@gmail.com

Website : https://github.com/yoo94