iam102 2023. 9. 10. 15:19

작업 중 Reference type(object or array)의 state를 Selector를 통해 변환하여 사용하는 경우 아래와 같은 warning을 접할 수 있습니다.

Selector customSelector returned a different result when called with the same parameters. This can lead to unnecessary rerenders. Selectors that return a new reference (such as an object or an array) should be memoized

// warning example
export const conformUserSelector = (state: RootState) => {
  return state.user.users.filter((user) => user.userType === UserType.worker);
};

// or

function Component() {
  const disabledUsers = useSelector(
    (state) => state.user.users.filter((user) => user.disable)
  );
  ...
}

함수형으로 코드를 작성하며 불변성을 지키기 위해 사용하는 map, filter 등의 메서드는 새로운 형태의 Reference type(object or array) 리턴하기 때문에 useSelector re-render 기준인 Strict equality (===)에서 다른 값으로 인식됩니다. 그래서 해당 값이 변하지 않더라도 dispatch가 발생하면 계속 re-render가 일어납니다.

 

 

Memoized Selectors with Reselect and createSelector

Redux에선 Reselect라는 라이브러리를 통해 최적화된 Selector를 생성해왔으며 이는 createSelector를 통해 제공됩니다. createSelector는 RTK(Redux Toolkit)에서도 사용할 수 있습니다.

 

createSelector 하나 이상의 input selectors output selector 통해 새로운 Selector 제공합니다. input selectors에는 dependency 필요한 요소를 여러  지정할 있으며 이는 output selector 전달 인자로 사용됩니다. output selector input selectors 전달된 인자를 변환하여 return 합니다. input selectors 인자가 이전 값들과 Strict equality (===) 조건을 통해 output selector 변환을 발생시킵니다.

 

예시 코드를 살펴보겠습니다.

// example Selector
import { createSelector } from '@reduxjs/toolkit';

export const conformUserSelector = createSelector(
  [(state) => state.user.users],
  (users) =>
    users.filter(
      (user: User) => user.userType === UserType.worker,
    ),
);

// multi input selectors
export const filterUsersSelector = createSelector(
  [
    (state) => state.user.users,
    (state) => state.user.targetID,
  ],
  (users, id) =>
    users.filter(
      (user: User) => user.id !== id,
    ),
);
// Example Component
import {
  conformUserSelector,
  filterUsersSelector,
} from '@/features/user/slice/userSlice';

...

function Component(props: { isLandScape?: boolean }) {
  const users = useSelector(conformUserSelector);
  const filterUsers = useSelector(filterUsersSelector);

  ...
}

export default Component;

 

createSelector를 통해 input selectors의 인자가 변경될 때만 Selector가 갱신 되도록 최적화를 할 수 있습니다.
다만 createSelector를 사용하면서 input selectors의 인자를 변환 없이 output selector로 바로 return 하는 케이스와 input selectors에서 state를 인자로 넘기는 케이스는 최적화를 할 수 없으니 금해야 합니다.

// Prevent Case
import { createSelector } from '@reduxjs/toolkit';

export const preventSelectorCase1 = createSelector(
  [(state) => state.user],
  (users) => users,
);

export const preventSelectorCase2 = createSelector(
  [(state) => state],
  (state) => 
    // mutate,
);

 

마무리

Selector를 사용하는 데 있어 최적화는 늘 고려해야 할 요소입니다. useSelector를 사용하면서 구조 분해 할당으로 선언하여 사용하기보단 useSelector를 나눠 사용해야하는 것과 react-redux의 shallowEqual를 사용하는 것은 Selector의 최적화를 위함입니다.

import { useSelector, shallowEqual } from 'react-redux';
 ... 
  // useSelector를 나눠서 선언 필요!
  const { targetID, users } = useSelector((state) => state.user);
  // 개선
  const targetID = useSelector((state) => state.user.targetID);
  const users = useSelector((state) => state.user.users);
 
  // 필요시 shallowEqual 사용
  const { targetID, users } = useSelector(
    (state) => state.user,
    shallowEqual,
  );
 ...

 

단순히 기능을 사용하는데 그칠 것이 아니라 어떻게 잘 쓸 것인지에 대해 늘 고민해야겠습니다.

 


참고 자료

https://redux.js.org/usage/deriving-data-selectors#optimizing-selectors-with-memoization

https://redux.js.org/usage/deriving-data-selectors#writing-memoized-selectors-with-reselect