React.memo로 리렌더링 방지하기

송은·2023년 6월 13일
0
post-thumbnail

React.memo를 이용하면 컴포넌트의 props가 바뀌지 않았다면, 리렌더링을 방지하여 컴포넌트의 리렌더링 성능 최적화를 해줄 수 있다.

이 함수를 사용하면 컴포넌트에서 리렌더링이 필요한 상황에서만 리렌더링 하도록 설정해줄 수 있다.

사용법

React.memo의 인자로 컴포넌트(함수)를 넣는다.

export default React.memo(CreateUser);
const CreateUser = ({ username, email, onChange, onCreate }) => {
  return (
    <div>
      <input
        name="username"
        placeholder="계정명"
        onChange={onChange}
        value={username}
      />
      <input
        name="email"
        placeholder="이메일"
        onChange={onChange}
        value={email}
      />
      <button onClick={onCreate}>등록</button>
    </div>
  );
};

export default React.memo(CreateUser);
const User = React.memo(function User({ user, onRemove, onToggle }) {
  return (
    <div>
      <b
        style={{
          cursor: 'pointer',
          color: user.active ? 'green' : 'black'
        }}
        onClick={() => onToggle(user.id)}
      >
        {user.username}
      </b>
      &nbsp;
      <span>({user.email})</span>
      <button onClick={() => onRemove(user.id)}>삭제</button>
    </div>
  );
});

function UserList({ users, onRemove, onToggle }) {
  return (
    <div>
      {users.map(user => (
        <User
          user={user}
          key={user.id}
          onRemove={onRemove}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
}

export default React.memo(UserList);

적용 후 input을 수정할 때 하단의 UserList가 리렌더링 되는지 확인해보았다.

🚨 그런데, User 중 하나라도 수정하면 모든 User들이 리렌더링되고, CreateUser도 리렌더링 되는 것을 볼 수 있었다.

👉 이유는 users 배열이 바뀔 때마다 onCreate도 새로 만들어지고, onToggle, onRemove도 새로 만들어지기 때문이다.

const onCreate = useCallback(() => {
  const user = {
    id: nextId.current,
    username,
    email
  };
  setUsers(users.concat(user));

  setInputs({
    username: '',
    email: ''
  });
  nextId.current += 1;
}, [users, username, email]);

const onRemove = useCallback(
  id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users.filter(user => user.id !== id));
  },
  [users]
);
const onToggle = useCallback(
  id => {
    setUsers(
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  },
  [users]
);

deps에 users가 들어있기 때문에 배열이 바뀔 때마다 함수가 새로 만들어진다. 이걸 최적화하고 싶다면 어떻게 해야할까?

deps에서 users를 지우고, 함수들에서 현재 useState로 관리하는 users를 참조하지 않게 하는 것이다.

👉 함수형 업데이트!

함수형 업데이트를 하게 되면, setUsers에 등록하는 콜백함수의 파라미터에서 최신 users를 참조할 수 있기 때문에 deps 에 users를 넣지 않아도 된다.

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setInputs(inputs => ({
      ...inputs,
      [name]: value
    }));
  }, []);
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]);

  const nextId = useRef(4);
  const onCreate = useCallback(() => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users => users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  }, [username, email]);

  const onRemove = useCallback(id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users => users.filter(user => user.id !== id));
  }, []);
  const onToggle = useCallback(id => {
    setUsers(users =>
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  }, []);
  const count = useMemo(() => countActiveUsers(users), [users]);
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

이렇게 해주면 특정 항목을 수정하게 될 때, 해당 항목만 리렌더링 될 것이다.

리액트를 개발할 때 useCallback, useMemo, React.memo는 컴포넌트의 성능을 실제로 개선할 수 있는 상황에서만 사용하는 것이 좋다.
예를 들어서 User 컴포넌트에 bbuttononClick으로 설정해준 함수들은, 해당 함수들을 useCallback으로 재사용한다고해서 리렌더링을 막을 수 있는 것은 아니므로, 굳이 그렇게 할 필요는 없다.
추가적으로, 렌더링 최적화하지 않을 컴포넌트에 React.memo를 사용하는 것은, 불필요한 props 비교만 하는 것이기 때문에 실제로 렌더링을 방지할 수 있는 상황이 있는 경우에만 사용한다.


propsAreEqual 함수

추가적으로, React.memo에서 두번째 파라미터에 propsAreEqual이라는 함수를 넣어서 특정값들만 비교를 하는 것도 가능하다.

이 함수에서는 prevProps(전 함수)와 nextProps(후 함수)를 가져와서 비교한다.

두 함수를 비교해서 true라면 리렌더링을 방지하고, false가 나온다면 리렌더링 된다.

export default React.memo(
  UserList,
  (prevProps, nextProps) => prevProps.users === nextProps.users
);

하지만, 이걸 잘못 사용한다면 오히려 의도치 않은 버그들이 발생하기 쉽다.

예를 들어서, 함수형 업데이트로 전환을 안했는데 이렇게 users만 비교를 하게 된다면, onToggleonRemove에서 최신 users 배열을 참조하지 않으므로 심각한 오류가 발생할 수 있다.

이런식으로 함수의 특정 props를 비교해서 리렌더링 방지를 할 수 있지만, 이 경우에 나머지 props들이 정말 고정적이기 때문에 비교가 할 필요가 없는 것인지 확인해야 한다.

profile
개발자

0개의 댓글