Redux Selector 최적화
작업 중 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