• Redux 에 reselect 적용해보기 :: 마이구미
    React 2020. 6. 7. 19:03
    이 글은 reselect 라이브러리를 다룬다.
    대다수의 경우 redux 를 사용하면서 reselect 를 도입하게 된다.
    왜 사용하는지, 어떤 개선점이 있고, 주의해야하는 사항은 무엇인지 알아본다.
    관련 문서 - https://github.com/reduxjs/reselect

     

    redux 공식문서에서 제공하는 기본 예제를 기반으로 연결되는 점들이 많다.

    글을 읽기 전에 먼저 훑어보면, 이해에 더 도움이 될 것이다.

    https://redux.js.org/basics/example

     

    우리는 상태 관리를 효율적으로 하기 위해 Redux 와 같은 상태 관리 라이브러리를 사용한다.

    기본적으로 리덕스 스토어있는 state 를 가져오는 방법은 다음과 같다.

    todoList 가 존재하고, todo 들의 완료 여부를 필터링할 수 있는 state 가 존재한다면 다음과 같다.

     

    const { todos, visibilityFilter } = useSelector(state => ({
        const todos = state.todos.map(...)
        return {
        	todos,
            visibilityFilter: state.visibilityFilter
        }
    }));
    
    return (
        {
            todos.map(....)
        }
    )

     

    스토어에는 todos, visibilityFilter 로 분류되어 있고, 이를 useSelector 를 통해 가져온다.

    그리고 todos 데이터를 루프를 통해 한번 가공한 후, 화면에 그린다고 가정한다.

     

    우선 useSelector 에서 대해 기억해야할 2가지는 다음과 같다.

    이 2가지는 그대로 문제점으로 연결될 수 있다.

     

    1. useSelector 는 랜더링마다 실행된다.
    2. 액션이 dispatched 되었을 때도 실행되어, 이전 반환값과 현재값이 다른 경우에 리랜더링을 집행하게 된다.

     

    다시 돌아가서 예제에서는 todos 를 통해 화면을 그리게 된다.

    그리고 액션을 통해 state 가 변경된 경우에는 리랜더링을 통해 화면을 업데이트할 것이다.

    하지만 위 코드는 이전 반환값과 현재 반환값이 같은 경우에도 리랜더링을 일으키게 된다.

    즉, 리랜더링이 집행되어 불필요한 리랜더링을 하게 되는 것이다.

     

    그리고 1번과 연관된 문제점으로 랜더링마다 실행된다는 것은 내부 로직을 매번 재계산을 한다는 것이다.

    이는 useSelector 내부에서 위와 같은 루프가 필요한 로직을 존재할 때, 데이터가 클수록 비용이 증가하게 된다.

    또한, 2번과도 연결되어 만약 불필요한 리랜더링이 일어난다면, 다시 실행되어 재계산하게 된다.

     

    그렇다면 우선 왜 이전 반환값과 현재 반환값이 같은 경우인데도 리랜더링을 일어났을까?

    이는 반환값을 단순히 오브젝트로 감싸는 것이 원인이 된다.

    useSelector 가 실행되면, 단순히 객체로 감싸진 반환값은 매번 새로운 주소를 가진 객체를 반환한다.

     

    const obj1 = {a: 1};
    const obj2 = obj1;
    const obj3 = {...obj1};
    
    obj1 === obj2 // true;
    obj1 === obj3 // false;

     

    주소값으로 비교하기 때문에, 새로 할당된 객체는 다른 주소를 가지고 있기 때문에 리랜더링을 집행하게 된다.

    이를 위해서 useSelector 를 2개로 나누어 객체를 반환하게 하지 않거나, redux 자체에서 제공해주는 shallowEqual 를 사용할 수 있긴하다.

    이 방법들은 글을 공식 문서에 더 친절하게 설명되어있다. (https://react-redux.js.org/api/hooks)

    그리고 그 중 마지막으로 언급된 방법이 reselect 이다.

     

     

    본인은 효율적인 방안으로는 reselect 를 사용하는 것이라 생각하고, 이는 memoizing selectors 라고 불린다.

    reselect 를 적용한 코드는 다음과 같다.

     

    const getTodo = state => state.todos;
    const getVisibilityFilter = state => state.visibilityFilter;
    
    const { todos, visibilityFilter } = createSelector(
        getTodo,
        getVisibilityFilter,
        (todos, visibilityFilter) => {
            const todos = state.todos.map(...)
            return {
                todos,
                visibilityFilter
            }
        }
    )

     

    reselect 는 넘어오는 인자 중 하나라도 변경이 되어야만 재계산을 하게 된다.

    createSelector 에 순서대로 정의된 selector 들의 반환값을 마지막 인자인 함수 형태의 인자들로 순서대로 넘겨받게 된다.

    todos, visibilityFilter 의 값이 하나라도 변해야만 실제 로직을 다시 계산하게 된다.

    이로써, 기존 코드에서 사용한 객체 형태로 반환하는 코드를 유지하면서 문제점들을 해결할 수 있다.

     

    또한, 큰 이점 중 하나로 selector 를 분리하는 것이다.

    이로 인해, selector 를 재사용할 수 있고, 서로 다른 셀렉터를 조합하여 사용할 수 있다는 것을 의미한다.

    READMD.md 에서 언급된 예제로 대체하겠다. (https://github.com/reduxjs/reselect)

     

    필요하다면 하나의 selector 를 여러 컴포넌트에서 공용으로 사용할 수도 있다.

    하지만 이러한 경우에 주의할 점이 있다.

    reselector 에서 제공하는 selector 의 캐시 사이즈는 1 이라는 것이다.

    이것이 어떤 문제를 초래할 수 있는가?

    다음 예제를 확인해보자.

     

     

     

    예제는 서로 다른 todoList 가 3개 존재하고, 각각에 대한 todos 가 존재하는 것이다.

    특정 selector 는 todoList 에 대한 고유 id 를 props 를 통해 받아 스토어에 있는 todoList 를 가져온다.

     

    const getVisibilityFilter = (state, props) => {
        return state.todoLists[props.listId].visibilityFilter;
    };
    
    const getVisibleTodos = createSelector(
      [getVisibilityFilter, getTodos],
      (visibilityFilter, todos) => {
          .....
      }
    );
    
    export default getVisibleTodos;

     

    각 TodoList 컴포넌트는 getVisibleTodos Selector 를 가지고 있다.

    이것이 의미하는 것은 각 컴포넌트는 이를 공용으로 사용하고 있다는 것이다.

    컴포넌트가 3개라면 listId 가 1, 2, 3 에 대한 selector 는 오직 하나만 존재하는 것이다.

    위에서 언급했듯이 캐시 사이즈는 1 이기 때문에, listId 가 1, 2, 3 인 경우의 각각의 경우에 대해 기억하고 있을 수가 없다.

    그래서 이러한 경우에는 매번 재계산을 하게 되서, 성능 이점을 가져올 수 없다.

    해결 방안은 단순히 selector 를 독립적으로 만들어주면 된다.

     

    const getVisibilityFilter = (state, props) => {
        return state.todoLists[props.listId].visibilityFilter;
    };
    
    const makeGetVisibleTodos = () => {
        return createSelector(
            [getVisibilityFilter, getTodos],
            (visibilityFilter, todos) => {
               .....
            }
        );
    }
    
    export default makeGetVisibleTodos;

     

    codesandbox 예제에서 App.js 파일에서 SharedVisibleTodoList 와 VisibleTodoList 를 구분해놓았다.

    각각 따로 import 해보고 콘솔을 통해 어떤 차이가 있는지 확인해보면 이해를 위해 훨씬 도움이 될 것이다.

     

    reselect 를 통해, selector 를 재사용과 조합해서 사용하려다보면, 많은 문제점이 발생할 수 있다.

    props 를 여러개 넘기거나 객체로 활용할 경우 또 다시 새로운 주소값이 할당되는 문제가 다시 발생한다.

    이와 관련되어 유용한 글이 될 수 있다.

    https://flufd.github.io/reselect-with-multiple-parameters/

     

    댓글 0

Designed by Tistory.