본문 바로가기

리액트

리액트 재랜더링 최적화

 

GPT가 참 기깔나게 썸네일 잘 뽑는다. 너는 어떤 최적화를 하고 있니..

1. 리액트의 재랜더링에 관하여

요즘 웹최적화에 약간 관심이 생기면서(자사서비스 개발로 이직한지 1년이 넘어가는데, 아직도 마인드는 대충 대충...) 리액트에서는 상태(state)나 프롭스(props)가 변경될 때 컴포넌트가 다시 렌더링된다. 다만 불필요한 재랜더링이 발생하면 성능 저하로 이어질 수 있다. 이게 앱이 작을 때는 별로 티가 안나지만, 점점 규모가 커질수록 퍼포먼스가 뚝뚝 떨어지는 게 보인다. 그러니, 초반에는 별로 신경쓰지 않고 개발하더라도 어느 시점에는 최적화가 필요한 시기가 오니 미리미리 익혀두는 게 좋다고 생각한다.

다만 무조건 재랜더링 방지 전략을 쓰는게 능사는 아니다. 왜냐하면 이건 공짜 점심이 아니고, 어디까지나 메모리 리소스를 쓰는 행위이기 때문. 선택과 집중을 잘하자.

🔥 비교 정리

최적화 기법
사용 목적
언제 사용하는가?
React.memo
컴포넌트 재랜더링 방지
프롭스가 변경되지 않을 때만 렌더링하고 싶을 때
useCallback
함수 재생성 방지
자식 컴포넌트에 함수를 프롭스로 넘길 때
useMemo
연산량이 많은 값 최적화
값이 변경될 때만 연산을 수행하고 싶을 때
useRef
렌더링 없이 값 유지
값은 유지하되 재랜더링은 하지 않고 싶을 때
Context 최적화
Context 값 변경 최소화
useContext 사용 시 불필요한 렌더링을 방지하고 싶을 때

 


2. 리액트 재랜더링의 주요 원인

  1. 상태(State) 변경: useState의 값이 변경되면 해당 컴포넌트와 하위 컴포넌트가 다시 렌더링됨.
  2. 부모 컴포넌트의 재랜더링: 부모 컴포넌트가 변경되면 자식 컴포넌트도 자동으로 재랜더링됨.
  3. 새로운 프롭스(Props) 전달: 자식 컴포넌트가 이전과 다른 프롭스를 받으면 다시 렌더링됨.
  4. 컨텍스트(Context) 변경: useContext를 사용 중일 때, 컨텍스트 값이 변경되면 해당 값을 사용하는 모든 컴포넌트가 다시 렌더링됨.
  5. 객체 및 함수의 참조 변화: useMemo나 useCallback을 사용하지 않으면 불필요한 객체 생성으로 인해 재랜더링이 발생할 수 있음.

3. 최적화 방법

1) React.memo : 컴포넌트 메모화

React.memo를 사용하면 프롭스가 변경되지 않는 한 컴포넌트가 다시 렌더링되지 않도록 할 수 있다.

아래의 코드에서 Child 컴포넌트는 count가 변경될 때만 다시 렌더링된다. memo를 쓰지 않으면, text가 변경되도 다시 렌더링이 되니 골때리는 결과가 나오게 된다.

import React from "react";

// 자식 컴포넌트
const Child = React.memo(({ count }) => {
  console.log("Child 렌더링");
  return <div>카운트: {count}</div>;
});

// 부모 컴포넌트
const Parent = () => {
  const [count, setCount] = React.useState(0);
  const [text, setText] = React.useState("");

  return (
    <div>
      <Child count={count} />
      <button onClick={() => setCount(count + 1)}>증가</button>
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
};
 

2) useCallback : 함수메모화 (함수 재생성 방지)

부모 컴포넌트가 다시 렌더링될 때, 함수가 새롭게 생성되면 이를 프롭스로 받는 자식 컴포넌트도 재랜더링된다.

이를 방지하려면 useCallback을 사용한다. 개인적으로 가장 자주 쓰게 되는 메모화이지 않을까 싶다.

// 부모 컴포넌트
const Parent = () => {
  const [count, setCount] = React.useState(0);

  const increment = React.useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  return <Child onIncrement={increment} />;
};

// 자식 컴포넌트
const Child = React.memo(({ onIncrement }) => {
  console.log("Child 렌더링");
  return <button onClick={onIncrement}>증가</button>;
});
 

3) useMemo: 변수메모화 (값 계산 최적화)

복잡한 계산이 필요한 경우, useMemo를 사용하여 불필요한 연산을 방지할 수 있다.

useMemo를 사용하면 num이 변경될 때만 연산이 수행된다.

const ExpensiveComponent = ({ num }) => {
  const expensiveCalculation = React.useMemo(() => {
    console.log("복잡한 연산 수행...");
    return num * 2;
  }, [num]);

  return <div>결과: {expensiveCalculation}</div>;
};
 

4) useRef: 상태 변경 없이 값 유지

useRef를 사용하면 상태를 변경하지 않고도 값을 유지할 수 있어 불필요한 재랜더링을 방지할 수 있다.

아래 코드에서 useRef를 사용하면 renderCount 값이 변경되어도 컴포넌트가 다시 렌더링되지 않는다.

되짚어 보면, 만약 useRef가 없다면 renderCount값이 변경되면 Component가 가진 상태값이 변하므로, Component는 재랜더링되고 말 것이다. 이걸 막는 것이다.

const Component = () => {
  const renderCount = React.useRef(0);
  renderCount.current += 1;

  return <div>렌더링 횟수: {renderCount.current}</div>;
};
 

5) Context 최적화

Context 값이 변경될 때, 해당 값을 구독하는 모든 컴포넌트가 다시 렌더링된다.

이를 막으려면 context를 여러 개로 분리하거나 useMemo를 사용해야 한다.

useMemo를 사용하면 UserContext.Provider의 값이 불필요하게 변경되는 것을 방지할 수 있다.

const UserContext = React.createContext();

const Parent = () => {
  const [user, setUser] = React.useState({ name: "Alice", age: 25 });

  const value = React.useMemo(() => ({ user, setUser }), [user]);

  return (
    <UserContext.Provider value={value}>
      <Child />
    </UserContext.Provider>
  );
};