reselect 패키지로 선택자 함수(selector function) 생성하기

katanazero·2020년 9월 21일
0

react

목록 보기
8/15
post-thumbnail
  • 리덕스에 있는 상태값을 화면에 보여 줄 때는 다양한 형식으로 가공할 필요가 있다. reselect 패키지는 원본 데이터를 다양한 형태로 가공해서 사용할 수 있도록 도와준다.

먼저 reselect 패키지 없이 해당 기능을 구현해보자.

  • /src/App.js
import React, {useCallback, useState} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import {additionAction, subtractionAction, divisionAction, multiplyAction} from './store/modules/calculator/index';
import {selectFruitAction} from './store/modules/fruit/index';
import './App.css';
import FruitSelect from "./components/FruitSelect";

export default function App() {

    const dispatch = useDispatch();
    const resultNum = useSelector(state => state.calculatorReducer.resultNum);

    // 데이터를 가공
    const [selectFruitString, selectFruitString2] = useSelector(state => {
        return [`${state.fruitReducer.selectFruit} 을 선택했어요!`, `${new Date().toISOString()}`]
    });

    const [aNum, setANum] = useState(0);
    const [bNum, setBNum] = useState(0);

    const onChangeANum = useCallback((e) => {
        setANum(Number(e.target.value));
    }, []);

    const onChangeBNum = useCallback((e) => {
        setBNum(Number(e.target.value));
    }, []);

    const onSelectItem = useCallback((item) => {
        dispatch(selectFruitAction({targetFruit : item}));
    },[]);


    return (
        <div className="App">
            <p>
                계산 결과값 : {resultNum}
            </p>
            <div>
                A : <input type='text' value={aNum} onChange={onChangeANum}/> <br/>
                B : <input type='text' value={bNum} onChange={onChangeBNum}/>
            </div>
            <div>
                <button onClick={() => dispatch(additionAction({aNum, bNum}))}>덧셈</button>
                <button onClick={() => dispatch(subtractionAction({aNum, bNum}))}>뺄셈</button>
                <button onClick={() => dispatch(divisionAction({aNum, bNum}))}>나눗셈</button>
                <button onClick={() => dispatch(multiplyAction({aNum, bNum}))}>곱하기</button>
            </div>
            <div>
                <FruitSelect options={['사과', '바나나', '딸기', '귤']} postfix={'원하는 과일을 선택해'} onChange={onSelectItem}/>
                선택한 과일 : {selectFruitString} <br/>
                {selectFruitString2}
            </div>
        </div>
    );
}
  • /src/store/modules/fruit/index.js
// action types
export const FRUIT_SELECT = `fruit/SELECT`;

// action creator function
export const selectFruitAction = ({targetFruit}) => ({type: FRUIT_SELECT, payload: {targetFruit}});


// state
const initialState = {
    selectFruit: ``
};

// reducer
const reducer = (state = initialState, action) => {

    switch (action.type) {

        case FRUIT_SELECT :
            return {
                ...state,
                selectFruit : action.payload.targetFruit
            }

        default :
            return state

    }

};

export default reducer
  • /src/components/FruitSelect.js
import React from "react";

// 사용자가 option 을 선택하면 이를 부모 컴포넌트에게 알려준다.

export default function FruitSelect({options, postfix, onChange}) {

    return (
        <div>
            <select onChange={event => onChange(event.target.value)}>
                {options.map((option, index) => <option key={index} value={option}>{option}</option>)}
            </select>
            {postfix}
        </div>
    )

}
  • /src/store/index.js
import {combineReducers, createStore} from 'redux';
import calculatorReducer from  './modules/calculator/index';
import fruitReducer from  './modules/fruit/index';

const rootReducer = combineReducers({
    calculatorReducer,
    fruitReducer
});

const store = createStore(rootReducer);

export default store
  • fruit 스토어 모듈을 추가로 작성 하였다.
  • FruitSelect 컴포넌트를 작성을 하였다.
    (간단하게 과일을 선택하면 부모 컴포넌트에게 알려준다.)
  • 부모 컴포넌트는 선택된 과일을 store 에 dispatch 하고 주어진 속성값을 화면에 그린다.
// 데이터를 가공
    const [selectFruitString, selectFruitString2] = useSelector(state => {
        return [`${state.fruitReducer.selectFruit} 을 선택했어요!`, `${new Date().toISOString()}`]
    });
  • 선택된 과일 상태값 (selectFruit) 을 이용해서 새로운 값으로 가공한다. 여기서 한가지 문제가 있는데, 액션이 발생하여 처리가 될때마다 selectFruitString2 를 만드는 연산이 매번 수행이 된다. 만약 이게 array 를 처리하는거라면 array 가 크면 클수록 불필요한 연산이 증가할 확률이 높다.
    (해당 예제에 포함된 덧셈 뺄셈을 눌러도 마찬가지다.. 시간이 계속 최신값으로 바뀐다.)
  • 다른 reducer 가 새로운 객체를 반환하더라도 모든 컴포넌트의 useSelector() 훅에 있는 선택자 함수가 불러진다고 유추할 수 있다.

reselect 패키지 사용하기

npm i -S reselect
  • /src/selectors/testSelector.js
import {createSelector} from "reselect";

const selectFruitSelector = state => state.fruitReducer.selectFruit;
const resultNumberSelector = state => state.calculatorReducer.resultNum;

export const memoizationSelector = createSelector([selectFruitSelector, resultNumberSelector], (selectFruit, resultNum) => {
    return `${new Date().toISOString()} : ${selectFruit} / ${resultNum}`
});
  • /src/App.js
import React, {useCallback, useState} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import {additionAction, subtractionAction, divisionAction, multiplyAction} from './store/modules/calculator/index';
import {selectFruitAction} from './store/modules/fruit/index';
import {createSelector} from "reselect";
import {memoizationSelector} from "./selectors/testSelector";

import './App.css';

// components
import FruitSelect from "./components/FruitSelect";

export default function App() {

    const dispatch = useDispatch();
    const resultNum = useSelector(state => state.calculatorReducer.resultNum);
    const selectFruit = useSelector(state => state.fruitReducer.selectFruit);

    // 데이터를 가공
    const [selectFruitString, selectFruitString2] = useSelector(state => {
        return [`${state.fruitReducer.selectFruit} 을 선택했어요!`, `${new Date().toISOString()}`]
    });

    // // reselect 를 사용
    const resultMemoizationSelector = useSelector(memoizationSelector);

    const resultMemoizationSelector2 = useSelector(state => memoizationSelector2(state));

    const [aNum, setANum] = useState(0);
    const [bNum, setBNum] = useState(0);

    const onChangeANum = useCallback((e) => {
        setANum(Number(e.target.value));
    }, []);

    const onChangeBNum = useCallback((e) => {
        setBNum(Number(e.target.value));
    }, []);

    const onSelectItem = useCallback((item) => {
        dispatch(selectFruitAction({targetFruit : item}));
    },[]);

    console.log('render..');

    return (
        <div className="App">
            <p>
                계산 결과값 : {resultNum}
            </p>
            <div>
                A : <input type='text' value={aNum} onChange={onChangeANum}/> <br/>
                B : <input type='text' value={bNum} onChange={onChangeBNum}/>
            </div>
            <div>
                <button onClick={() => dispatch(additionAction({aNum, bNum}))}>덧셈</button>
                <button onClick={() => dispatch(subtractionAction({aNum, bNum}))}>뺄셈</button>
                <button onClick={() => dispatch(divisionAction({aNum, bNum}))}>나눗셈</button>
                <button onClick={() => dispatch(multiplyAction({aNum, bNum}))}>곱하기</button>
            </div>
            <div>
                <FruitSelect options={['사과', '바나나', '딸기', '귤']} postfix={'원하는 과일을 선택해'} onChange={onSelectItem}/>
                선택한 과일 : {selectFruitString} <br/>
                {selectFruitString2}
                <hr/>
                <p>reselect(숫자값 바꾸고 누르면 시간이 갱신된다.)</p>
                {resultMemoizationSelector}
                <br/>
                {resultMemoizationSelector2}
            </div>
        </div>
    );
}

// reselect
const resultNumberSelector = state => state.calculatorReducer.resultNum;
const memoizationSelector2 = createSelector([resultNumberSelector], resultNum => `${resultNum} 결과입니다!! ${new Date().toISOString()}`);
  1. createSelector 함수를 이용해서 선택자 함수를 작성한다.
import { createSelector } from 'reselect';
createSelector(...inputSelectors | [inputSelectors], resultFunc)
  1. createSelector 함수는 첫번째 인자로 셀렉터함수를 받고, 두번째 인자는 값을 입력받아서 처리하는 함수다.
  2. reselect 는 메모이제이션 기능이 있기 때문에, 연산에 사용되는 값이 변경된 경우에만 연산을 수행하고 변경되지 않았다면 이전 결과값을 재사용한다.
  • 왜 export default 내부에 작성을 하지 않았나요?
const [selectFruitString, selectFruitString2] = useSelector(state => {
        return [`${state.fruitReducer.selectFruit} 을 선택했어요!`, `${new Date().toISOString()}`]
 });

위 코드부분이 매번 렌더링을 발생시킨다. 그래서,

const resultNumberSelector = state => state.calculatorReducer.resultNum;
const memoizationSelector2 = createSelector([resultNumberSelector], resultNum => `${resultNum} 결과입니다!! ${new Date().toISOString()}`);

매번 이 코드도 초기화가 이루어져서 메모이제이션이 의도한대로 동작을 하지 않는다.

그래서 예제는 바깥쪽에 작성을 하였으며, 매번 렌더링을 발생시키는 코드를 주석처리하고 위 코드를 옮겨오면 의도한대로 정상동작을 한다.

import React from "react";
import {createSelector} from 'reselect'
import {useSelector} from "react-redux";

function TestSelector2({text}) {

    const resultNumSelector = state => state.calculatorReducer.resultNum;
    const dummySelector = () => text;
    const resultNumString = createSelector([resultNumSelector, dummySelector], (resultNum, text) => `${text} 가 변해야 새로운 변신! ${new Date().toISOString()}`);
    const memoSelector = useSelector(state => resultNumString(state));

    return (
        <p>
            {memoSelector}
        </p>
    )

}

export default React.memo(TestSelector2)

이런형식으로 값을 가공할 때, 컴포넌트의 속성값을 이용할 수도 있다.

적절한 예제가 아닌데, 이거 말고 각 컴포넌트마다 선택하는걸 제한을 한다던가 필터링하는기능을 구현할때 유용할거 같다. (이부분은 각 컴포넌트에서 특정 속성값에 따라, 가공하는 데이터가 조금씩 필터링을 다르게 하고 싶을때 구현하면 좋을거 같다)


  • reducer 에서 가공하여 스토어 상태에 저장하여 사용하기

모든 경우의 수를 처리하면 기본적으로 메모리를 많이 사용하게 되는 문제가 있으며, 더 큰 문제는 같은 데이터가 여러 곳에 중복으로 저장된다는 점이다. 중복인 경우에 만약 목록에 속성항목내용이 변경되면 동기화를 해줘야하는데 이또한 복잡하고 놓치면 버그가 생길 수 있다.

  • render 에서 가공해서 사용하기

장점은 원본 데이터만 저장하면 되기 때문에 메모리를 적게 쓴다. 단점은 매번 render() 함수가 호출되면 연산을 해야하기 때문에 성능에 좋지 않다.

-> reselect 를 잘 활용해보자..

profile
developer life started : 2016.06.07 ~ 흔한 Front-end 개발자

0개의 댓글