-
Redux Selector 최적화Frameworks, Platforms and Libraries/React 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
'Frameworks, Platforms and Libraries > React' 카테고리의 다른 글
React Query 도입기 (0) 2023.11.09 useState와 useRef의 차이 (0) 2023.10.29 서버 상태 관리를 위한 라이브러리 선택 (0) 2023.10.15 Redux Toolkit으로 상태 관리 하기 (0) 2023.08.20 useEffect vs useLayoutEffect (0) 2023.08.06