부끄럽지만 최근까지 나는 react-redux
의 useSelector
를 다음과 같이 구조 분해 할당하여 사용해왔다.
const { id, name } = useSelector((state: RootState) => state.user)
이와 같이 스토어의 슬라이스를 전부 갖고 와서 그 중 필요한 값만 구조 분해 할당으로 빼왔다.
최근에 불필요한 리렌더링을 잡기 위해 데브툴을 켜놓고 만져보던 도중, 리렌더링이 전혀 발생하지 않아야 할 곳들이 반짝반짝하는걸 봤다.
useSelector
가 문제 될 줄은 전혀 모르고 삽질만 하다가, "혹시 ?" 하는 마음에 검색해봤다가 알게 되었다.
여러분은 지금까지 useSelector
를 잘못 사용해왔습니다.
useSelector
로state.user
까지만 가져온 후 구조 분해 할당으로 값을 빼올 경우,state.user
값 중 하나라도 변경되면 리렌더링이 발생한다.
state.user
를 전부 갖고 왔기 때문에 name, photo만 사용한다고 하더라도 state.user
가 업데이트되면 같이 리렌더링 된다.
따라서 useSelector
+ 구조 분해 할당은 useSelector
의 대표적인 안티 패턴이다.
공식 문서에 따르면 3가지 방법을 제시한다.
한 컴포넌트에서
useSelector
를 여러 번 사용할 수 있습니다. 사실, 굉장히 좋은 방법입니다.useSelector
는 항상 가능한한 제일 작은 크기의 값을 반환해야 합니다.
공식 문서에서 사실상 추천하는 방법이라고 생각한다.
![]()
React-Redux
에서 제공하는shallowEqual
함수를equalityFn
으로써useSelector
에 전달한다.
eaulityFn
은 useSelector
의 두번째 인자로 전달할 수 있는 비교 함수이다.
equalityFn?: (left: any, right: any) => boolean
equalityFn
을 사용하면 이전 값과 다음 값을 equalityFn
을 통해 비교해서 false
가 나올 때만 리렌더링 한다.
shallowEqual
은 이름에서 알 수 있듯이 얕은 비교를 수행하는 함수이다.
import { shallowEqual, useSelector } from 'react-redux'
// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
하지만 다음과 같은 중첩 객체가 있을 때 shallowEqual
은 children이 변할 때만 감지하고, 그 안에 son이나 daughter의 변화 여부는 감지하지 않는다.
const object = {
id: 1,
name: 'Jason',
children: {
son: {
id: 2,
name: 'Bob'
},
daughter: {
id: 3,
name: 'Emily'
}
}
};
깊은 비교가 필요할 경우 lodash의 isEqual을 사용하거나 다음과 같이 커스텀 함수를 만들어야 한다.
import { useSelector } from 'react-redux'
// equality function
const customEqual = (oldValue, newValue) => oldValue === newValue
// later
const selectedData = useSelector(selectorReturningObject, customEqual)
![]()
Reselect
또는 비슷한 라이브러리를 사용해서 메모이즈된 셀렉터를 생성합니다. 이 셀렉터는 한 객체에 다수의 값을 반환하지만, 값 중 하나라도 변경이 될 때에만 새로운 객체를 반환합니다.
즉, 값이 변경될 때에만 새로운 객체를 반환하고, 그 외에는 메모이즈된 객체를 반환한다.
이 방법은 단순히 값만 불러오는 것 보다는, 불러오는 동시에 복잡한 연산이 있을 때 또는 새로운 객체를 반환해야 할 때 권장되는 방법인 것 같다.
다음과 같이 idList를 반환하는 배열의 경우, useSelector가 사용될 때마다 (값이 변동 여부와는 상관 없이) 항상 새로 계산되어 새로운 객체가 반환된다.
const idList = useSelector((state: RootState) => state.user.idList.map((id) => id === targetId));
이럴 경우 메모이제이션은 불필요한 리소스 방지에 큰 도움이 될 수 있다.
Reselect
라이브러리의 메모이제이션을 사용할 수 있지만, Redux Toolkit
에는 기본적으로 createSelector
가 내장되어 있다.
const idSelector = createSelector(state, list => {
return list.map(({ id }) => id === targetId);
});
const idList = useSelector(idSelector);
조사하면 할수록 나는 상당히 혼란스러웠다. 그래서 도대체 뭘 쓰라는거지? 싶었다.
useSelector
를 여러 번 쓰면 간편하겠지만, 가독성을 해칠 것 같고 코드 볼륨이 커지지 않을까 걱정되었다.
equalityFn
은 얕은 비교, 깊은 비교를 매번 생각하면서 만들어야하기 때문에 공수가 너무 많이 들 것 같았다.
createSelector
는 앞에서 말했듯이 단순히 값만 불러오는 것 보다 불러오는 동시에 복잡한 연산이 있을 때 권장되는 방법인 것 같았다. 값만 불러오는데도 메모이제이션을 사용한다면, 메모이제이션을 너무 많이 사용하는게 아닐지 우려가 되었다. 또한 오히려 사용성이 더 복잡해지는게 아닐까 걱정되었다.
여기 저기 알아본 결과, 나는 다음과 같이 결론 내렸다.
useSelector
를 여러번 사용하는 것이 제일 낫다.createSelector
의 메모이제이션을 사용한다.equalityFn
을 설정해주기 번거롭거나 얕은 비교만으로 커버가 가능할 경우 shallowEqual
사용을 고려한다.답답한 마음에 제로초님께도 질문을 올렸고, 1번을 제일 선호하신다는 답변을 받았다.