상태관리 Redux (3)

dante Yoon·2022년 1월 16일
1

redux

목록 보기
3/3
post-thumbnail

Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance를 읽고 개인 의견으로 살을 붙여 정리한 포스팅입니다.

Reselect, selectors

본 포스팅은 올바른 방법으로 Reselect, selectors를 사용하기 위해 알아야 할 내용에 초점을 맞추어 작성되었습니다.

Basic of selectors

리덕스에서selector function 이란 리덕스 스토어의 상태값을 인자 값으로 받아 가공하여 데이터를 반환하는 함수를 말한다. 함수를 작성함에 있어 엄격하게 지켜야 하는 규칙은 없지만, 관례적인 함수 네이밍으로 select, get을 프리픽스로 사용한다.

selector 함수를 사용하는 이유

재사용성, 캡슐화

selector function을 만들어 사용하는 이유는 action creators, middleware를 작성하여 사용하는 이유와 동일하게 재사용성과 캡슐화를 충족시키기 위함이다.

예를들어 컴포넌트 파일인 SomeComponent.tsx 에서 인라인 함수로 작성한 mapState가 또다른 컴포넌트에서 동일한 기능을 하는 selector function으로 따로 만들어진다면, 스토어 데이터 구조가 변경되었을 때 관리 포인트가 컴포넌트 갯수에 비례하여 커지게 된다. *협업의 관점에서 셀렉터 함수(selector function)을 별도의 모듈로 분리하는 것은 컴포넌트 작성자에게 함수의 내부 구현에 집중하지 않고 컴포넌트 작성에 집중하게 도와준다.

성능 개선

리엑트 함수형 컴포넌트 내부의 인라인 함수에서는 useMemo, useCallback을 이용해 함수의 선언 값과 반환 값을 메모라이징 하는데, 이는 리렌더링 시 재선언과 재계산을 방지하기 위함이다. 이와 약간 다른 개념으로 selector function은 리렌더링을 유발시키는 것을 방지하기 위해 만든다.

Memoization is a form of caching. It involves tracking inputs to a function, and storing the inputs and the results for later reference. If a function is called with the same inputs as before, the function can skip doing the actual work, and return the same result it generated the last time it received those input values.

인용

다음의 셀렉터 함수는 스토어에 컴포넌트와 관계 없는 상태 값을 업데이트 하기 위해 스토어에 새로운 action이 dispatch될 때마다 불필요하게 반복하여 실행된다.

const mapState = (state) => {
    const {someData} = state;

    const filteredData = expensiveFiltering(someData);
    const sortedData = expensiveSorting(filteredData);
    const transformedData = expensiveTransformation(sortedData);

    return {data : transformedData};
}

Reselect

앞서 본 mapState 작성자의 입장에서 someData 필드 값의 변경이 발생했을 때해당 함수가 재실행되는 것이 좋을 것이다. Reselect는 memoized selector function을 만드는 것을 도와준다.

createSelector 함수는 여러 개의 selector function들을 첫번째 인자로 받아들이는데 배열에 들어가는 셀렉터 함수들을 input selector라고 하며, input selector의 반환값이 output selector라고 불리는 두번째 인자의 인자 값들이 된다.

const selectA = state => state.a;
const selectB = state => state.b;
const selectC = state => state.c;

const selectABC = createSelector(
    [selectA, selectB, selectC],
    (a, b, c) => {
        // do something with a, b, and c, and return a result
        return a + b + c;
    }
);

// 다음의 함수는 selector function을 받는 형식이 배열이 아닐 뿐, selectABC와 동일하다.
const selectABC2 = createSelector(
    selectA, selectB, selectC,
    (a, b, c) => {
        // do something with a, b, and c, and return a result
        return a + b + c;
    }
);

input selectors의 반환 값이 이전의 반환값과 다른 경우에만 (===) output selector를 재실행 시킨다.


다음의 예제코드에서 보듯이 createSelector로 생성된 selector들은 input selector에 필요한 인자값들의 수만큼 인자를 받을 수 있다. 아래에서 selectItemId는 state 말고도 itemId를 파라메터로 받는데, 이는
selectItemById(state,42)와 같이 함수를 호출함으로 써 두번째 인자값을 selectItemId에 전달할 수 있다.

const selectItems = state => state.items;  
const selectItemId = (state, itemId) => itemId;  
  
const selectItemById = createSelector(  
    [selectItems, selectItemId],  
    (items, itemId) => items[itemId]  
);  

const item = selectItemById(state, 42);

/*
Internally, Reselect does something like this:

const firstArg = selectItems(state, 42);  
const secondArg = selectItemId(state, 42);  
  
const result = outputSelector(firstArg, secondArg);  
return result;  
*/

따라서 createSelector의 input selectors의 시그니처 중 input의 타입은 서로 호환이 가능해야 한다.

Optimizing Performance With Reselect

다음은 Reselect 라이브러리를 이용해 selector function 사용할 때 성능 향상을 야기시킨 것이다.

const selectSomeData = state => state.someData;

const selectFilteredSortedTransformedData = createSelector(
    selectSomeData,
    (someData) => {
         const filteredData = expensiveFiltering(someData);
         const sortedData = expensiveSorting(filteredData);
         const transformedData = expensiveTransformation(sortedData);

         return transformedData;
    }
)

const mapState = (state) => {
    const transformedData = selectFilteredSortedTransformedData (state);

    return {data : transformedData};
}

헷갈리는 부분이 있을 수 있어 위의 로직이 어떤 부분에서 성능 개선에 도움이 되었는지 짚고 넘어가면 좋겠다.

  • 로직에도 명료하게 드러나있지만, state.someData가 변경되었을 경우에만 selectFilteredSortedTransformedData가 재실행되어 오버헤드가 큰 작업의 불필요한 재실행을 방지했다.
  • react-redux의 connect 함수는 mapState 함수의 반환 값을 이전 값과의shallow equality 비교를 통해 컴포넌트를 리렌더링 시킨다. 따라서 mapState 함수가 반환하는 배열 값이 concat, map, filter와 같이 항상 새로운 참조 값을 반환한다면, 실제 state 변환과 상관없이 모든 action이 dispatch될 때마다 컴포넌트의 리렌더링을 유발한다. 컴포넌트의 리렌더링이 필요한지에 대한 판단을 createSelector가 적용된 selector에 위임하고, mapState는 그저 반환 데이터 구조를 만드는 것에만 집중할 수 있다.

방금 작성한 두번째 부분은 굉장히 중요한 부분이라고 생각한다. 클린 소프트웨어를 만드는데 필요한 Single Responsibility Principle이 적용된 부분이라고 생각하기에 한번 곱씹어서 읽어보았으면 좋겠다.

Advanced Optimizations with React-Redux

리셀렉트(Reselect)를 사용할때 주의해야할 점이 있다.
createSelector의 인스턴스는 이전 호출되었을 때의 인자 값을 기반으로 memoize된 값을 반환할지의 여부를 판단한다. 다음의 코드를 보자

// someSelector는 createSelector를 사용해 생성된 인스턴스이다. 
const a = someSelector(state, 1); // first call, not memoized
const b = someSelector(state, 1); // same inputs, memoized
const c = someSelector(state, 2); // different inputs, not memoized
const d = someSelector(state, 1); // different inputs from last time, not memoized

Reselector를 이용헀지만 a,b를 선언할때만 메모라이징이 되었다.

재사용성과 캡슐화를 충족시키기 위해 createSelector의 인스턴스를 selectItemForThisComponent로 모듈화해 사용한다고 가정하자.

const mapState = (state, ownProps) => {
    const item = selectItemForThisComponent(state, ownProps.itemId);

    return {item};
}

const SomeComponent = (props) => <div>Name: {props.item.name}</div>;

export default connect(mapState)(SomeComponent);

// later
<SomeComponent itemId={1} />
<SomeComponent itemId={2} />

여러개의 SomeComponent 인스턴스가 렌더링되는데, itemId가 다르기 때문에
SomeComponent가 렌더링 될때는 Reselect가 주는 메모라이징의 이점을 전혀 사용할 수가 없다. 항상 동기적으로 다음과 같이 selectItemForThisComponent가 호출되기 때문이다.

// first instance
selectItemForThisComponent(state, 1);
// second instance
selectItemForThisComponent(state, 2);

따라서 fully optimized 된 컴포넌트를 사용하기 위해서는 createSelector의 인스턴스를 각기 다른 컴포넌트에서 선언해 사용해주어야 한다.

react-redux의 connect 함수는 mapState, mapDispatch에서 factory function 사용을 지원한다.

mapState, mapDispatch 함수가 객체가 아닌 함수를 반환한다면 connect 함수는 전달된 함수의 반환 값을 mapState, mapDispatch의 실제 함수 값으로 사용한다.

팩토리 함수를 선언함으로써 component-instance-specific selectors를 사용할 수 있다.

const makeUniqueSelectorInstance = () => createSelector(
    [selectItems, selectItemId],
    (items, itemId) => items[itemId]
);    


const makeMapState = (state) => {
    const selectItemForThisComponent = makeUniqueSelectorInstance();

    return function realMapState(state, ownProps) {
        const item = selectItemForThisComponent(state, ownProps.itemId);

        return {item};
    }
};

export default connect(makeMapState)(SomeComponent);
profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글