본문 바로가기

카테고리 없음

[React] React 성능 최적화 기법: 초보자를 위한 상세 가이드

반응형

 

오늘은 React 애플리케이션의 성능을 개선하는 방법에 대해 자세히 알아보겠습니다. React는 이미 빠른 라이브러리지만, 앱이 커지고 복잡해지면 성능 문제가 발생할 수 있어요. 이런 문제를 해결하고 더 빠른 앱을 만들기 위한 다양한 기법들을 하나씩 살펴보겠습니다.

1. 불필요한 렌더링 방지하기

React에서 성능 저하의 주요 원인 중 하나는 불필요한 렌더링입니다. 컴포넌트가 실제로 변경되지 않았는데도 다시 그려지는 경우가 있죠. 이를 방지하는 몇 가지 방법을 알아봅시다.

React.memo 사용하기

React.memo는 컴포넌트의 props가 변경되지 않았다면 리렌더링을 방지합니다. 간단히 말해, 입력이 같다면 같은 결과를 반환한다는 거죠.

const MyComponent = React.memo(function MyComponent(props) {
  console.log("렌더링됨!");
  return <div>{props.name}</div>;
});

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>카운트 증가</button>
      <MyComponent name="John" />
    </div>
  );
}

이 예제에서 버튼을 클릭해도 MyComponent는 다시 렌더링되지 않습니다. props가 변경되지 않았기 때문이죠.

useMemo 훅 사용하기

useMemo는 계산 비용이 높은 작업의 결과를 기억해둡니다. 예를 들어, 큰 배열을 정렬하는 작업을 매번 하는 대신 한 번만 하고 그 결과를 재사용할 수 있죠.

function ExpensiveComponent({ data }) {
  const sortedData = useMemo(() => {
    console.log("데이터 정렬 중...");
    return [...data].sort((a, b) => a - b);
  }, [data]); // data가 변경될 때만 다시 계산합니다

  return <div>{sortedData.join(", ")}</div>;
}

이 컴포넌트는 data prop이 변경될 때만 정렬 작업을 수행합니다.

useCallback 훅 사용하기

useCallback은 함수를 메모이제이션합니다. 즉, 불필요하게 새로운 함수를 만들지 않도록 해줍니다.

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

  const handleClick = useCallback(() => {
    console.log("클릭!");
  }, []); // 의존성 배열이 비어있으므로, 이 함수는 항상 같은 참조를 유지합니다

  return (
    <div>
      <ChildComponent onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>카운트: {count}</button>
    </div>
  );
}

이렇게 하면 ParentComponent가 리렌더링되더라도 handleClick 함수는 새로 생성되지 않습니다.

2. 상태 관리 최적화

상태 분리하기

모든 상태를 하나의 객체에 넣는 대신, 관련 있는 상태끼리 묶어서 관리하면 불필요한 리렌더링을 줄일 수 있습니다.

// 좋지 않은 예
const [state, setState] = useState({
  count: 0,
  name: '',
  items: []
});

// 좋은 예
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [items, setItems] = useState([]);

이렇게 하면 count가 변경되어도 name이나 items와 관련된 컴포넌트는 리렌더링되지 않습니다.

Context API 적절히 사용하기

Context API는 props drilling을 피하기 위해 자주 사용되지만, Context의 값이 자주 변경되면 성능 문제가 발생할 수 있습니다. 따라서 자주 변경되는 값은 지역 상태로, 광범위하게 사용되는 값은 Context로 관리하는 것이 좋습니다.

const ThemeContext = React.createContext('light');

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <Header />
      <Main />
      <Footer />
    </ThemeContext.Provider>
  );
}

이 예제에서 theme은 자주 변경되지 않는 값이므로 Context를 사용하기에 적합합니다.

3. 가상화(Virtualization) 사용하기

긴 목록을 렌더링할 때는 화면에 보이는 항목만 렌더링하는 것이 효율적입니다. 이를 '가상화'라고 하며, react-window나 react-virtualized 같은 라이브러리를 사용하여 구현할 수 있습니다.

import { FixedSizeList as List } from 'react-window';

function Row({ index, style }) {
  return <div style={style}>항목 {index}</div>;
}

function VirtualizedList() {
  return (
    <List
      height={150}
      itemCount={1000}
      itemSize={35}
      width={300}
    >
      {Row}
    </List>
  );
}

이 예제는 1000개의 항목을 가진 목록을 렌더링하지만, 실제로는 화면에 보이는 항목만 렌더링됩니다.

4. 코드 스플리팅(Code Splitting)

대규모 애플리케이션의 경우, 모든 코드를 한 번에 로드하는 대신 필요할 때마다 코드를 불러오는 것이 효율적입니다. 이를 '코드 스플리팅'이라고 하며, React.lazy와 Suspense를 사용하여 구현할 수 있습니다.

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <React.Suspense fallback={<div>로딩 중...</div>}>
      <OtherComponent />
    </React.Suspense>
  );
}

이렇게 하면 OtherComponent는 MyComponent가 렌더링될 때 비로소 로드됩니다.

5. 불변성(Immutability) 유지하기

React는 객체의 참조가 변경되었을 때 변화를 감지합니다. 따라서 상태를 업데이트할 때는 새로운 객체를 만들어 주는 것이 좋습니다.

// 좋지 않은 예
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // 이렇게 하면 React가 변화를 감지하지 못합니다
setItems(items);

// 좋은 예
setItems([...items, 4]); // 새로운 배열을 만들어 줍니다

이렇게 하면 React가 변화를 정확히 감지하고 필요한 부분만 업데이트할 수 있습니다.

6. 이벤트 핸들러 최적화

이벤트 핸들러를 인라인으로 정의하면 매 렌더링마다 새로운 함수가 생성됩니다. 대신 메서드로 정의하면 이를 방지할 수 있습니다.

// 좋지 않은 예
<button onClick={() => handleClick(id)}>클릭</button>

// 좋은 예
<button onClick={this.handleClick}>클릭</button>

클래스 컴포넌트에서는 메서드를, 함수형 컴포넌트에서는 useCallback을 사용하여 이 문제를 해결할 수 있습니다.

7. 프로파일링 도구 사용하기

React DevTools의 Profiler를 사용하면 어떤 컴포넌트가 자주 렌더링되는지, 렌더링에 얼마나 시간이 걸리는지 등을 확인할 수 있습니다. 이를 통해 성능 최적화가 필요한 부분을 정확히 찾아낼 수 있죠.

  1. Chrome 개발자 도구를 엽니다 (F12 또는 Ctrl+Shift+I).
  2. React DevTools 탭으로 이동합니다.
  3. Profiler 탭을 선택합니다.
  4. 녹화 버튼을 누르고 앱을 사용합니다.
  5. 녹화를 멈추고 결과를 분석합니다.

React 애플리케이션의 성능을 최적화하는 방법은 다양합니다. 불필요한 렌더링을 방지하고, 상태 관리를 최적화하며, 가상화와 코드 스플리팅을 활용하는 등의 기법을 적절히 사용하면 더 빠르고 효율적인 애플리케이션을 만들 수 있습니다.

 

하지만 기억해야 할 중요한 점이 있습니다. 바로 "조기 최적화는 모든 악의 근원"이라는 말이죠. 실제로 성능 문제가 발생하기 전에 과도하게 최적화하려고 하면 오히려 코드가 복잡해지고 유지보수가 어려워질 수 있습니다.

따라서 항상 측정 가능한 문제에 대응하여 최적화를 진행해야 합니다. 프로파일링 도구를 활용하여 실제로 성능 저하가 발생하는 부분을 정확히 파악한 후, 적절한 최적화 기법을 적용하는 것이 가장 효과적인 접근 방식입니다.

 

마지막으로, React 성능 최적화는 끝이 없는 여정입니다. 새로운 버전의 React가 나오고 새로운 최적화 기법이 등장하면서 계속해서 학습해야 합니다. 하지만 이 글에서 소개한 기본적인 최적화 기법들을 이해하고 적용한다면, 대부분의 상황에서 충분히 빠른 React 애플리케이션을 만들 수 있을 거예요. 화이팅!

반응형