useSelector 최적화로 리렌더링 줄이기

hahagarden·2023년 8월 21일
1

useSelector 동작 방식

Redux store의 전역상태를 useSelector Hooks로 가져와 사용할 수 있다. useSelector는 컴포넌트가 렌더링되거나 action이 dispatch 될 때마다 selector 함수를 실행하고 반환값을 비교한다.
selector 함수는 store의 state를 인자로 받고 useSelector Hooks는 selector 함수가 반환한 값을 반환한다.

Fortunately, useSelector automatically subscribes to the Redux store for us! That way, any time an action is dispatched, it will call its selector function again right away. If the value returned by the selector changes from the last time it ran, useSelector will force our component to re-render with the new data. All we have to do is call useSelector() once in our component, and it does the rest of the work for us.

However, there's a very important thing to remember here:
CAUTION
useSelector compares its results using strict === reference comparisons, so the component will re-render any time the selector result is a new reference! This means that if you create a new reference in your selector and return it, your component could re-render every time an action has been dispatched, even if the data really isn't different.
Redux 공식문서

리렌더링 낭비

  1. selector 함수가 state => state 이면, store 전체를 비교하는 것이므로 현재 컴포넌트와 상관이 없는 다른 상태가 변경되어도 상태가 업데이트된 것으로 간주하여 컴포넌트가 리렌더링된다.
  2. selector 함수가 반환하는 값이 참조값이면 실제로 값이 달라지지 않았는데도 참조가 달라졌을 경우 값이 바뀐 것으로 인식하여 리렌더링된다. (i.e. filter 메서드로 반환되는 배열을 반환하는 경우)
const { isLoggedIn } = useAppSelector((state) => state);
  const { isOpenChatItemModal, isOpenScrapbookModal, isOpenScrapModal, isOpenWithdrawalModal } = useAppSelector(
    (state) => state
  );
  const { number, diff } = useSelector(
    state => ({
      number: state.counter.number,
      diff: state.counter.diff
    })
  );

개선 방법

multiple useSelector

const isOpenChatItemModal = useAppSelector((state) => state.isOpenChatItemModal);
  const isOpenScrapbookModal = useAppSelector((state) => state.isOpenScrapbookModal);
  const isOpenScrapModal = useAppSelector((state) => state.isOpenScrapModal);
  const isOpenWithdrawalModal = useAppSelector((state) => state.isOpenWithdrawalModal);

독립적으로 선언된 상태들은 서로의 영향을 받지 않아 낭비 렌더링을 막을 수 있다.
공식 문서에서도 추천하고 있는 방법이다.

We can call useSelector multiple times within one component. In fact, this is actually a good idea - each call to useSelector should always return the smallest amount of state possible.

shallowEqual 사용하기

  const { number, diff } = useSelector(
    state => ({
      number: state.counter.number,
      diff: state.counter.diff
    }),
    shallowEqual
  );
const a = { foo: "bar" }
const b = { foo: "bar" }

console.log( a === b ) // will log false
console.log( shallowEquals(a, b)) // will log true

shallowEqual은 객체의 가장 바깥을 비교한다.
첫 번째 코드는 매번 새로운 객체를 반환하기 때문에 상태 값이 바뀌었는지와 무관하게 매번 새로운 참조를 가지므로 리렌더링이 발생한다. 이 때 shallowEqual을 사용하여 값들을 비교하여 값의 변화가 없을 때 리렌더링을 방지한다.

RTK의 createSelector 사용하기

const state = {
  a: {
    first: 5
  },
  b: 10
}

const selectA = state => state.a
const selectB = state => state.b

const selectA1 = createSelector([selectA], a => a.first)

const selectResult = createSelector([selectA1, selectB], (a1, b) => {
  console.log('Output selector running')
  return a1 + b
})

const result = selectResult(state)
// Log: "Output selector running"
console.log(result)
// 15

const secondResult = selectResult(state)
// No log output
console.log(secondResult)
// 15

createSelector 함수는 메모이제이션을 활용한다.
input selector와 output selector를 인자로 받아 새로운 select 함수를 반환한다.
resultsecondResult를 보면 selectA1selectB의 결과값이 그대로 510이기 때문에 secondResult의 output selector가 실행되지 않고 memoized result가 반환 된 것을 볼 수 있다.

memoization을 사용하는 것이기 때문에 연산이 복잡할 때와 같이 효율을 고려해서 사용하는 것이 좋겠다.

참고

useSelector 사용시 성능 문제점 문의드립니다.
useSelector의 적절한 사용법에 대해 궁금합니다.
Redux의 useSelector 최적화 하기
How Exactly useselector works?
8. useSelector 최적화

profile
공부한 내용을 기록합니다. 틀린 정보 피드백은 언제나 감사합니다 🌷 이전 블로그 https://hahagarden.tistory.com/

0개의 댓글