React
createSelector를 이용해 useSelector를 효율적으로 사용하기
안녕하세요 redux-toolkit을 사용하여 대규모 프로젝트에서 웹소켓 데이터와 복잡한 클라이언트 상태를 관리하게 된 경험을 바탕으로 redux-toolkit에서 제공하는 createSelector에 대해서 알아보겠습니다.
이러한 환경에서 상태 관리를 효율적으로 수행하기 위해서는 적절한 도구와 패턴이 필요합니다. 특히, redux-toolkit은 복잡한 상태 관리를 단순화 할 수 있는 여러 기능을 제공합니다. 그중에서도 createSelector는 선택자(selector)를 생성하는 데 사용 되어, 상태를 효율적으로 추출하고 재사용할 수 있게 해줍니다.
createSelector
createSelector는 Redux 상태에서 필요한 데이터를 효율적으로 추출하기 위한 도구로, 선택자(selector) 함수를 생성하는데 사용하는 유틸리티 함수입니다.
이 함수의 주요 장점은 결과를 메모이제이션(memoization)하여, 동일한 입력에 대해 반복적으로 계산하지 않고 이전 결과를 재사용함으로써 성능을 개선할 수 있다는 것입니다.
즉, createSelector를 사용하면 복잡한 상태 변화 로직을 단순화하고, 컴포넌트가 필요로 하는 데이터를 더욱 효율적으로 얻을 수 있습니다. 특히, 상태가 자주 업데이트 되는 대규모 애플리케이션, 웹 게임, 웹소켓을 사용하여 Redux 상태를 다룰 때 성능을 최적화하는 데 매우 유용합니다.
selector와 컴포넌트 렌더링의 관계
import { createSelector } from '@reduxjs/toolkit'
// 예시 상태
const initialState: ShopItemState = {
items: [
{ id: 1, name: '신발', category: '명품' },
{ id: 2, name: '맨투맨', category: '보세' },
{ id: 3, name: '반팔티', category: '명품' },
{ id: 4, name: '모자', category: '아웃도어' },
// ... 매우 많은 항목
]
}
// 컴포넌트 구현부
function ItemList() {
const expensiveItems = useSelector(
(state) => state.shop.items.filter(item => item.category === '명품')
)
...
}
위 예시를 보겠습니다.
ItemList 컴포넌트에 작성된 useSelector 훅은 Redux 스토어의 상태를 구독하여, 특정 상태 조각을 선택하는 역할을 합니다. 이 경우, state.shop.items에서 category가 "명품"인 항목들을 필터링하여 expensiveItems에 할당하고 있습니다.
이 접근 방식에는 몇 가지 고려해야 할 사항이 있습니다.
- 자동 구독:
useSelector훅은 Redux 스토어의 상태가 변경될 때마다 자동으로 구독을 갱신합니다. 이는 컴포넌트가 상태 변경을 감지하게 되고, 그에 따라 리렌더링되도록 만듭니다. - 매번 새로운 인스턴스 생성: 현재
useSelector훅 사용 방식에서는 컴포넌트가 리렌더링될 때마다filter메서드가 호출될 것이고, 새로운 배열이 생성됩니다. 이는 불필요한 연산을 초래하고, 위 예시에서 처럼 많은 항목을 다루는 경우 성능 저하로 이어질 수 있습니다. 매번 필터링을 수행하는 것은 비용이 높은 작업이므로, 렌더링 성능이 저하될 수 있습니다. - 메모이제이션 필요성: 이러한 성능 문제를 해결하기 위해,
createSelector를 사용하여 메모이제이션된 셀렉터를 생성하는 것이 좋습니다. 메모이제이션을 통해 동일한 입력에 대해 이전 결과를 재사용할 수 있어, 불필요한 계산을 줄이고 최적화된 렌더링을 구현할 수 있습니다.
createSelector 사용하기
// 모든 아이템을 가져오는 기본 셀렉터
const selectAllItems = (state: RootState) => state.shop.items;
// 특정 카테고리의 항목을 필터링하는 비용이 높은 셀렉터
const selectItemsByCategory = createSelector(
// 두 번째 인자로 카테고리 받음
[selectAllItems, (state, category) => category],
(items, category) => {
// 필터링 로직 ( 비용이 높은 연산 )
return items.filter(item => item.category === category);
}
);
const selectAllItems = (state: RootState) => state.shop.items;
위 코드에서는 selectAllItems 라는 모든 아이템 항목을 가져오는 기본 셀렉터를 정의합니다.
const selectItemsByCategory = createSelector(
// 두 번째 인자로 카테고리 받음
[selectAllItems, (state, category) => category],
(items, category) => {
// 필터링 로직 ( 비용이 높은 연산 )
return items.filter(item => item.category === category);
}
);
위 코드에서는 createSelector 함수를 사용하여 더 비용이 높은 셀렉터를 정의합니다.
createSelector는 두 개의 인자를 받습니다.
- 입력 셀렉터 배열
- 첫 번째 인자는 하나 이상의 셀렉터 함수의 배열입니다. 위 예시에서는 두 개의 셀렉터가 포함되어 있습니다.
selectAllItems: 이 셀렉터는 Redux 스토어에서 모든 항목을 가져오는 역할을 합니다. 이 셀렉터는 상태의 일부를 직접적으로 반환합니다.(state, category) => category: 이 익명 함수는 두 번째 인자로 전달된 category 값을 반환합니다. 이 함수는 상태를 인자로 받아서 카테고리 정보를 가져옵니다. 이렇게 하면 사용자가 선택한 카테고리를 셀렉터에 전달할 수 있습니다.
- 결과 함수
- 두 번째 인자는 입력 셀렉터에서 반환된 값을 인자로 받아 최종적으로 계산된 값을 반환하는 함수입니다. 위 코드에서는 필터링 로직을 포함하고 있습니다.
(items, category) => {...}이 함수는 items와 category를 인자로 받아, 주어진 카테고리와 일치하는 항목만 필터링하여 반환합니다.items.filter(item => item.category === category): 이 부분에서, items 배열을 순회하여 각 항목의 category가 주어진 category와 동일한 경우에만 해당 항목을 포함하는 새로운 배열을 생성합니다. 이 연산은 비용이 높은 연산으로, 특히 항목 수가 많을 경우 성능에 영향을 줄 수 있습니다. 위와 같이createSelector를 사용하면selectAllItems셀렉터의 결과를 캐싱하여 중복 계산을 피하고 성능을 개선할 수 있습니다.
열심히 만든 셀렉터는 아래처럼 사용합니다.
const expensiveItems = useSelector((state: ItemType) => selectItemsByCategory(state, '명품'))
createSelector, useShallow
두 함수는 각각 상태 관리에 있어 성능 최적화라는 같은 목표로 하지만, 접근 방식과 사용 사례가 다릅니다.
- createSelector: 메모이제이션을 통해 동일한 입력에 대해 이전 결과를 재사용함으로써, 비용이 높은 계산을 피하고 성능을 개선합니다. 복잡한 상태 변환이나 대량의 데이터를 필터링할 때 유용하며, 상태 변화에 따른 리렌더링을 효율적으로 관리할 수 있습니다.
- useShallow: Zustand의 useShallow는 상태 변경 시 얕은 비교를 수행하여, 상태 객체의 특정 부분이 변경된 경우에만 컴포넌트를 리렌더링합니다. 이를 통해 불필요한 리렌더링을 방지하면서도 간단한 상태 선택을 가능하게 합니다. 저도 비슷한 줄 알고 있었는데 포스팅을 준비하며 비교해보니 접근 방식이 다르다는 것을 알게 되어서 저와 같은 사람을 위해 비교를 해봤습니다.
복잡한 상태 관리와 메모이제이션이 필요한 대규모 애플리케이션에서는 createSelecotr가 유리한 것 같고, 간단한 상태 관리와 얕은 비교를 통해 성능 최적화를 원하는 경우에는 useShallow가 적합하다고 생각되네요
마무리하며
React를 처음 공부할 때 Redux를 많이 사용하였으나, 평소에는 react-query와 zustand를 주로 사용하여 서버 상태와 클라이언트 상태를 관리해왔습니다.
최근 회사 프로젝트에서 많은 애니메이션과 패널 관리 등 복잡한 UI 상태를 다뤄야하는 대규모 프로젝트에서 ReduxToolkit을 다시 사용해 보았습니다.
그 결과, 대규모 프로젝트에서는 Redux Toolkit의 장점이 더욱 두드러지는 것을 느꼈습니다. 특히 상태 관리가 복잡해질수록 Redux Toolkit의 구조화된 접근 방식과 강력한 미들웨어 기능이 큰 도움이 되었습니다.
이처럼 무조건 쉽고 사용성이 좋은 도구만을 좋게 생각하는 것 보다는 각 도구의 특성과 장단점을 잘 이해하고 적절히 활용하는 것이, 애플리케이션의 요구 사항에 맞춰 효율적이고 유지 보수 하기 쉬운 코드를 작성하는 데 중요한 요소임을 다시 한 번 깨닫게 되었습니다.
앞으로도 상황에 맞는 도구를 선택하여 최적의 솔루션을 찾는 데 주력할 것을 다짐하며
마치겠습니다.