createSelector는 Reselect 라이브러리로부터 비롯됐으며, 사용하기 쉽게 재배포된 것이다.
그리고 Reselect 라이브러리를 살펴보면 createSelector
는 selector
함수를 memoization할 수 있도록 도와준다.
A library for creating memoized "selector" functions
// postsSlice
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { sub } from 'date-fns';
import axios from 'axios';
const POSTS_URL = 'https://jsonplaceholder.typicode.com/posts';
const initialState = {
posts: [],
status: 'idle', // "idle" | "loading" | "successed" | "failed"
error: null,
count: 0,
};
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
reactionAdded(state, action) {
const { postId, reaction } = action.payload;
const existingPost = state.posts.find((post) => post.id === postId);
if (existingPost) {
existingPost.reactions[reaction]++;
}
},
increaseCount(state, action) {
state.count = state.count + 1;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state, action) => {
state.status = 'loading';
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeed';
// Adding date and reactions
let min = 1;
const loadedPosts = action.payload.map((post) => {
post.date = sub(new Date(), { minutes: min++ }).toISOString();
post.reactions = {
thumbsUp: 0,
wow: 0,
heart: 0,
rocket: 0,
coffee: 0,
};
return post;
});
// Addd any fetched posts to the array
state.posts = state.posts.concat(loadedPosts);
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
// ...
},
});
export const getCount = (state) => state.posts.count;
export const selectAllPosts = (state) => state.posts.posts;
export const selectPostById = (state, postId) =>
state.posts.posts.find((post) => post.id === postId);
export const { increaseCount, reactionAdded } = postsSlice.actions;
export default postsSlice.reducer;
// Header Component
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from 'react-redux'
import { increaseCount, getCount } from '../features/posts/postsSlice'
function Header() {
const dispatch = useDispatch();
const count = useSelector(getCount);
return (
<header className="Header">
<h1>Redux Blog</h1>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/post">Post</Link></li>
<li><Link to="/user">Users</Link></li>
<li>
<button onClick={() => dispatch(increaseCount())}>
{count}
</button>
</li>
</ul>
</nav>
</header>
)
}
export default Header
// UserPage Component
const postsForUser = useSelector(state => {
const allPosts = selectAllPosts(state)
return allPosts.filter((post) => post.userId === Number(userId))
});
HeaderComponent의 count 상태를 변경시켰는데 UserPage 컴포넌트도 재렌더링이 되는 것으로 확인된다.
이는 useSelector
가 실행될 때마다 필터 함수는 매번 새로운 배열(유저 목록)을 반환하게 되면서 이전에 참조하고 있던 객체 주소가 현재 주소와의 차이를 발생시키게 된다. 그리고 re-rendering을 발생시키는데 이때 재계산이 필요한 상태 트리의 사이즈나 계산 비용이 크다면 성능 문제로 이어질 수 있다.
이러한 문제를 회피하기 위해서 createSelector
를 사용하면 애플리케이션을 최적화할 수 있다.
이제 memoized selector를 만들어 보자!
// postsSlice
export const selectPostsByUser = createSelector(
[selectAllPosts, (state, userId) => userId],
(posts, userId) => posts.filter((post) => post.user === userId),
);
export const selectPostsByUser = createSelector(
selectAllPosts,
(state, userId) => userId,
(posts, userId) => posts.filter((post) => post.userId === userId),
);
createSelector
에 사용할 state들을 선언하는 과정이 필요하다. selectPostsByUsers
에 posts
와 userId
를 사용할 예정이기 때문에 selectAllPosts와 (state, userId) => userId
를 선언해주었다.
이렇게 각각의 셀렉터들을 넣고, 마지막 인자는 state를 매개변수로 받는 함수의 형태인데 컴포넌트에서 useSelector가 인수로 받는 형태와 동일하다. n개의 인자를 받으면, (n-1)번째 인자까지는 새로운 값을 계산하는데 필요한 state를 받는다.
filter 함수를 사용해서 새로운 배열을 반환하게 되어 재렌더링이 일어나는 문제를 해결했다!😆
createSelector
함수가 memoization, 즉 이전에 계산한 값을 메모리에 저장하여 값이 변경됐을 경우에만 계산하도록 동작하는 것을 확인할 수 있다.