그럼에도 불구하고

👨‍💻

[Redux with TS] createSelector에 대해 알아보자 본문

React/Redux

[Redux with TS] createSelector에 대해 알아보자

zenghyun 2023. 7. 27. 21:03

 

RTK의 createSelector에 대해 알아보겠습니다.

 

🧑🏻‍💻 createSelector

createSelector는 리덕스 스토어 상태에서 데이터를 추출할 수 있도록 도와주는 유틸리티 함수입니다. 이를 통해 계산 비용이 높은 셀렉터(Selector)함수의 결과를 캐싱하여 성능을 최적화할 수 있습니다.

 

createSelector 함수는 Reselect 라이브러리에 의해 제공됩니다.

 

Reselect는 Redux 애플리케이션의 상태(state)가 바뀔 때만 해당 셀렉터를 다시 계산하도록 제어하여 중복 계산을 방지합니다.

 

기존의 useSelector의 경우 컴포넌트가 상태를 처리하는 작업이며, 컴포넌트가 리렌더링 되는 문제를 야기할 수 있습니다. 이런 문제를 미연에 방지하기 위해, createSelector는 상태 변경 작업을 리덕스에서 미리 처리해 둬서 컴포넌트에서는 그냥 가져다 쓰게 도와주는 것입니다.

 

즉, createSelector는 메모이제이션 기능을 제공하는 유틸리티 함수입니다. 

 

⭐️ state 가져오는 것과 컴포넌트의 렌더링은 무슨 관계일까?

아래 예시를 보겠습니다.

const users = useSelector(
  (state) => state.users.filter(user => user.subscribed)
);

컴포넌트의 구현부에 작성된 인라인 useSelector 훅은 스토어를 자동으로 구독하고 있기 때문에 상태 트리가 갱신되어 컴포넌트를 다시 render 해야 되는 경우 매번 새로운 인스턴스를 생성하게 됩니다. 

 

위 예제에서는 subscribed user를 조회하기 위해서 filter 함수를 사용하고 있습니다. useSelector가 실행될 때마다 필터 함수는 매번 새로운 배열을 반환하게 되면서 이전에 참조하고 있던 객체 주소가 현재 주소와의 차이를 발생시키게 됩니다. 그리고는 re-rendering을 발생시키는데 이때 재계산이 필요한 상태 트리의 사이즈나 계산 비용이 크다면 성능 문제로 이어질 수 있습니다. 

 

 

⭐️ createSelector 사용 예시

1
2
3
4
5
6
export const selectAllPosts = (state: RootStateType) => state.posts.posts;
 
export const selectPostsByUser = createSelector(
  [selectAllPosts, (_state, userId) => userId],
  (posts, userId) => posts.filter(post => post.user === userId)
);
cs

 

export const selectAllPosts = (state: RootStateType) => state.posts.post;

이 코드는 selectAllPosts 라는 일반적인 Redux 셀렉터를 정의합니다. 이 셀렉터는 RootStateType라는 전체 상태 객체에서 posts라는 프로퍼티에 접근하여 포스트들의 배열을 반환합니다. 

 

export const selectPostsByUser = createSelector(
  [selectAllPosts, (_state, userId) => userId],
  (posts, userId) => posts.filter(post => post.user === userId)
);

이 코드는 createSelector 함수를 사용하여 더 복잡한 셀렉터를 정의합니다.

selectPostsByUser 셀렉터는 특정 사용자(user) ID에 해당하는 포스트들을 선택하는 데 사용됩니다. 

 

createSelector 함수는 두 개의 인자를 받습니다. 첫 번째 인자는 입력 셀렉터들의 배열이며, 두 번째 인자는 최종 결과를 계산하는 함수입니다. 입력 셀렉터들은 해당 셀렉터 함수의 인자로 전달되며, 최종 결과를 계산하는 함수는 입력 셀렉터들의 결과를 받아 처리합니다. 

 


 

📌 createSelector의 첫 번째 인수는 항상 배열로 받아야 하나요? 

 

네 그렇습니다. 이 배열은 입력 셀렉터(input selectors)들의 리스트입니다. 입력 셀렉터는 최종 결과를 계산하는 함수의 인자로 사용되며, 해당 셀렉터 함수의 반환 값이 캐시 되고, 상태가 변경될 때만 다시 계산됩니다.

 

배열로 입력 셀렉터들을 전달하는 이유는 두 가지입니다. 

 

1. 캐시 관리: 배열로 전달된 입력 셀렉터들의 결과를 캐시 하여 중복 계산을 방지합니다. 상태(state)가 변경되지 않는 한 동일한 입력 셀렉터들은 이전에 계산된 결과를 사용하게 됩니다.

 

2. 의존성 명시: 입력 셀렉터들은 최종 결과를 계산하는 함수의 인자로 사용되기 때문에, 이를 통해 해당 셀렉터 함수가 어떤 상태를 의존하고 있는지 명시할 수 있습니다. Redux 상태가 변경되었을 때, 해당 셀렉터 함수는 입력 셀렉터들의 결과가 변경되었는지 확인하여 새로운 값을 계산하거나 캐시 된 값을 사용합니다. 

 


 

위의 코드에서는 두 개의 입력 셀렉터를 정의하고 있습니다.

 

[selectAllPosts, (_state, userId) => userId]

첫 번째 입력 셀렉터로 selectAllPosts를 사용하고, 두 번째 입력 셀렉터는 익명의 함수입니다. 두 번째 입력 셀렉터는 현재 상태를 나타내는 _state와 userId를 인자로 받습니다. 이 두 값을 이용하여 최종 결과를 계산할 때 쓰이게 됩니다. 

 

(posts, userId) => posts.filter(post => post.user === userId)

두 번째 인자는 최종 결과를 계산하는 함수입니다. 이 함수는 selectAllPosts의 결과인 posts 배열과 두 번째 입력 셀렉터로부터 받은 userId를 이용하여 해당 사용자의 포스트들을 필터링하여 반환합니다. 

 

 

이렇게 createSelector을 사용하면 selectPostsByUser 셀렉터가 동일한 userId에 대해 여러 번 호출되더라도, selectAllPosts 셀렉터의 결과를 캐싱하여 중복 계산을 피하고 성능을 개선할 수 있습니다. 

 

Redux 상태가 변경되면 새로운 값을 계산하고, 변경이 없을 경우 이전에 캐싱된 값을 사용합니다. 이를 통해 복잡한 셀렉터의 성능을 최적화하면서도 동일한 입력에 대해 일관된 결과를 유지할 수 있습니다. 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { UserListType, UserStateType, selectUserById } from "../../features/users/usersSlice";
import { selectPostsByUser } from "../../features/posts/postsSlice";
import { useParams } from "react-router-dom";
import { RootStateType } from "../../app/store";
 
const UserPage = () => {
  const param = useParams();
 
  const user = useSelector((state: UserListType) =>
    selectUserById(state, param.userId)
  ) as UserStateType;
 
  const postsForUser = useSelector((state:RootStateType) => 
    selectPostsByUser(state, param.userId));
    
  const postTitles = postsForUser.map(post => (
    <li key={post.id}>
        <Link to={`/posts/${post.id}`}>{post.title}</Link>
    </li>
  ))
 
  return (
    <section>
        <h2>{user.name}</h2>
        <ul>{postTitles}</ul>
    </section>
  )
};
 
export default UserPage;
 
cs

 

createSelector로 만든 selectUserById는 아래와 같이 사용하면 됩니다. :)

const user = useSelector((state: UserListType) =>
    selectUserById(state, param.userId)
  ) as UserStateType;

 

 

🏷️ Ref 

https://blog.hwahae.co.kr/all/tech/tech-tech/6946

https://velog.io/@rohkorea86/RTK-createSelector

 

Comments