createSelector로 selector를 memoization하기!

허재원·2022년 12월 4일
0
post-thumbnail

createSelector?

createSelector는 Reselect 라이브러리로부터 비롯됐으며, 사용하기 쉽게 재배포된 것이다.
그리고 Reselect 라이브러리를 살펴보면 createSelectorselector 함수를 memoization할 수 있도록 도와준다.

A library for creating memoized "selector" functions

createSelector 적용 전 코드

// 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

React Profiler를 사용하여 성능 측정하기

// 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를 사용하면 애플리케이션을 최적화할 수 있다.

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들을 선언하는 과정이 필요하다. selectPostsByUserspostsuserId를 사용할 예정이기 때문에 selectAllPosts와 (state, userId) => userId를 선언해주었다.
이렇게 각각의 셀렉터들을 넣고, 마지막 인자는 state를 매개변수로 받는 함수의 형태인데 컴포넌트에서 useSelector가 인수로 받는 형태와 동일하다. n개의 인자를 받으면, (n-1)번째 인자까지는 새로운 값을 계산하는데 필요한 state를 받는다.

적용 후 성능 측정하기

filter 함수를 사용해서 새로운 배열을 반환하게 되어 재렌더링이 일어나는 문제를 해결했다!😆
createSelector 함수가 memoization, 즉 이전에 계산한 값을 메모리에 저장하여 값이 변경됐을 경우에만 계산하도록 동작하는 것을 확인할 수 있다.

0개의 댓글