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>
);
}
// 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
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>
)
}
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() 훅에 있는 선택자 함수가 불러진다고 유추할 수 있다.
npm i -S reselect
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}`
});
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()}`);
- createSelector 함수를 이용해서 선택자 함수를 작성한다.
import { createSelector } from 'reselect'; createSelector(...inputSelectors | [inputSelectors], resultFunc)
- createSelector 함수는 첫번째 인자로 셀렉터함수를 받고, 두번째 인자는 값을 입력받아서 처리하는 함수다.
- reselect 는 메모이제이션 기능이 있기 때문에, 연산에 사용되는 값이 변경된 경우에만 연산을 수행하고 변경되지 않았다면 이전 결과값을 재사용한다.
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)
이런형식으로 값을 가공할 때, 컴포넌트의 속성값을 이용할 수도 있다.
적절한 예제가 아닌데, 이거 말고 각 컴포넌트마다 선택하는걸 제한을 한다던가 필터링하는기능을 구현할때 유용할거 같다. (이부분은 각 컴포넌트에서 특정 속성값에 따라, 가공하는 데이터가 조금씩 필터링을 다르게 하고 싶을때 구현하면 좋을거 같다)
모든 경우의 수를 처리하면 기본적으로 메모리를 많이 사용하게 되는 문제가 있으며, 더 큰 문제는 같은 데이터가 여러 곳에 중복으로 저장된다는 점이다. 중복인 경우에 만약 목록에 속성항목내용이 변경되면 동기화를 해줘야하는데 이또한 복잡하고 놓치면 버그가 생길 수 있다.
장점은 원본 데이터만 저장하면 되기 때문에 메모리를 적게 쓴다. 단점은 매번 render() 함수가 호출되면 연산을 해야하기 때문에 성능에 좋지 않다.
-> reselect 를 잘 활용해보자..