❤벨로퍼트 리액트 - 6장. 리덕스
8) useSelector 최적화
리액트 컴포넌트에서 리덕스 상태를 조회해서 사용할때 최적화를 하기 위해 고려해야하는 사항.
할일목록 - 프리젠테이셔널 컴포넌트에서 React.memo 사용하여 리렌더링 최적화.
컨테이너 컴포넌트에서는?
useSelector를 사용해 리덕스 스토어의 상태를 조회할때는 상태가 바뀌지 않았으면 리렌더링 하지 않는다.
TodosContainer 의 경우 카운터 값이 바뀔때 todos에는 변화가 없으므로 리렌더링 되지 않는것.
(const todos = useSelector(state ⇒ state.todos ).
CounterContainer의 경우 useSelector를 통해 매번 렌더링 될때마다 새로운 객체 {number, diff} 를 만들기때문에 상태가 바뀌었는지 바뀌지 않았는지 알수 없어서 매번 리렌더링되는것.
const { number, diff } = useSelector(state => ({
number: state.counter.number,
diff: state.counter.diff
}));
⇒ 이를 최적화 하기 위한 방법
1. useSelector 여러번 사용
const number = useSelector(state => state.counter.number);
const diff = useSelector(state => state.counter.diff);
위의 경우 해당 값들중 적어도 하나가 바뀌었을때만 컴포넌트가 리렌더링 된다. (렌더링 낭비를 줄일 수 있다)
2. react-redux의 shallowEqual 함수를 useSelector의 두번째 인자로 전달
import React from 'react';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease, setDiff } from '../modules/counter';
function CounterContainer() {
const { number, diff } = useSelector(
state => ({
number: state.counter.number,
diff: state.counter.diff
}),
shallowEqual
);
(...)
useSelector의 두번째 파라미터 - equalityFn
equalityFn?: (left: any, right: any) => boolean
이전 값과 다음 값을 비교하여 true이면 리렌더링 X, false이면 리렌더링 O
shallowEqual : react-redux에 내장된 함수. 객체 안의 가장 겉에 있는 값들을 비교함
ex)
const object = {
a: {x: 3, y: 2, z: 1},
b: 1,
c: [{ id: 1}]
}
// 가장 겉에 있는 값 - object.a, object.b, object.c
// shallowEqual은 위 세가지만 비교하고 내부의 값(ex- object.a.x , object.c[0])은
// 비교하지 않는다.
9) connect 함수
(1) 소개
connect 함수 : 컨테이너 컴포넌트를 만드는 방법. 클래스형 컴포넌트로 작성하는 경우, Hooks를 사용할 수 없기 때문에 connect 함수를 사용한다.
2019년 이전에 작성된 리덕스와 컴포넌트들은 connect로 작성되었을 것이므로 유지보수를 하게 될 경우 접하게 될 수 있다.
(2) HOC란?
Higher-Order Component. 리액트 컴포넌트를 개발하는 하나의 패턴. 특정 함수 또는 값을 props로 받아와서 사용하고 싶은 경우 등 컴포넌트의 로직을 재활용할때 유용하다.
Hook이 도입되기 전에는 자주 사용되어왔으나 지금은 대부분의 경우 Hook이 대체가능하기때문에 잘 사용하지 않는다.
HOC의 용도는 컴포넌트를 특정 함수로 감싸서 특정 값 또는 함수를 props로 받아와서 사용할 수 있게 해주는 패턴이다.
예시
const enhance = withState('counter', 'setCounter', 0)
const Counter = enhance(({ counter, setCounter }) =>
<div>
Count: {counter}
<button onClick={() => setCounter(n => n + 1)}>Increment</button>
<button onClick={() => setCounter(n => n - 1)}>Decrement</button>
</div>
)
withState 함수를 사용해 enhance컴포넌트에 props로 특정 값과 함수를 넣어주는 함수. 컴포넌트를 만들때 enhance로 감싸서 원하는 값과 함수를 props로 사용할수 있게 한다.
(3) connect 사용해보기
connect는 리덕스 스토어 안에 있는 상태를 props로 넣어주기도 하고 액션을 디스패치하는 함수를 props로 넣어주기도 한다.
CounterContatiner를 connect로 구현한 예제 - CounterContainer2.js
import React from "react";
import { connect } from 'react-redux';
import Counter from "../components/Counter";
import { decrease, increase, setDiff } from "../modules/counter";
function CounterContainer2({ number, diff, onIncrease, onDecrease, onSetDiff}) {
return (
<Counter
// 상태와
number={number}
diff={diff}
// 액션을 디스패치하는 함수들을 props로 넣어줌
onIncrease={onIncrease}
onDecrease={onDecrease}
onSetDiff={onSetDiff}/>
);
};
// mapStateToProps : 리덕스 스토어의 상태를 조회해서 어떤 것들을 props로 넣어줄지 정의
// 현재 리덕스 상태를 파라미터로 받아옴
const mapStateToProps = state => ({
number: state.counter.number,
diff: state.counter.diff
});
// mapDispatchToProps : 액션을 디스패치 하는 함수를 만들어서 props로 넣어줌
// dispatch를 파라미터로 받아옴
const mapDispatchToProps = dispatch => ({
onIncrease: () => dispatch(increase()),
onDecrease: () => dispatch(decrease()),
onSetDiff: diff => dispatch(setDiff(diff)),
});
// connect 함수에는 mapStateToProps, mapDispatchToProps를 인자로 넣는다
export default connect(
mapStateToProps,
mapDispatchToProps
)(CounterContainer2);
// 위 코드는 다음과 같다
// const enhance = connect(mapStateToProps, mapDispatchToProps);
// export defualt enhance(CounterContainer);
- mapStateToProps : 컴포넌트에 props로 넣어줄 리덕스 스토어 상태 관련 함수
- mapDispatchToProps : 컴포넌트에 props로 넣어줄 액션을 디스패치하는 함수들에 관련된 함수
mapDispatchToProps를 bindActioncreators(redux 라이브러리에 내장) 사용하여 리팩토링
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from 'react-redux';
import Counter from "../components/Counter";
import { decrease, increase, setDiff } from "../modules/counter";
// 액션 생성함수 이름이 변경되어 props 이름도 변경됨
// ex) onIncrease -> increase
function CounterContainer2({ number, diff, increase, decrease, setDiff}) {
return (
<Counter
// 상태와
number={number}
diff={diff}
// 액션을 디스패치하는 함수들을 props로 넣어줌
onIncrease={increase}
onDecrease={decrease}
onSetDiff={setDiff}/>
);
};
// mapStateToProps : 리덕스 스토어의 상태를 조회해서 어떤 것들을 props로 넣어줄지 정의
// 현재 리덕스 상태를 파라미터로 받아옴
const mapStateToProps = state => ({
number: state.counter.number,
diff: state.counter.diff
});
// mapDispatchToProps : 액션을 디스패치 하는 함수를 만들어서 props로 넣어줌
// dispatch를 파라미터로 받아옴
const mapDispatchToProps = dispatch =>
// bindActionCreators : 사용시 자동으로 액션 생성함수에 dispatch가 감싸진 상태로 호출할 수 있다.
bindActionCreators(
{
increase,
decrease,
setDiff
},
dispatch
);
// ({
// onIncrease: () => dispatch(increase()),
// onDecrease: () => dispatch(decrease()),
// onSetDiff: diff => dispatch(setDiff(diff)),
// });
// connect 함수에는 mapStateToProps, mapDispatchToProps를 인자로 넣는다
export default connect(
mapStateToProps,
mapDispatchToProps
)(CounterContainer2);
// 위 코드는 다음과 같다
// const enhance = connect(mapStateToProps, mapDispatchToProps);
// export defualt enhance(CounterContainer);
connect 에서는 mapDispatchToProps가 함수가 아니라 객체형태일때는 bindActionCreators를 대신 호출해준다.
예제코드
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from 'react-redux';
import Counter from "../components/Counter";
import { decrease, increase, setDiff } from "../modules/counter";
// 액션 생성함수 이름이 변경되어 props 이름도 변경됨
// ex) onIncrease -> increase
function CounterContainer2({ number, diff, increase, decrease, setDiff}) {
return (
<Counter
// 상태와
number={number}
diff={diff}
// 액션을 디스패치하는 함수들을 props로 넣어줌
onIncrease={increase}
onDecrease={decrease}
onSetDiff={setDiff}/>
);
};
// mapStateToProps : 리덕스 스토어의 상태를 조회해서 어떤 것들을 props로 넣어줄지 정의
// 현재 리덕스 상태를 파라미터로 받아옴
const mapStateToProps = state => ({
number: state.counter.number,
diff: state.counter.diff
});
// mapDispatchToProps가 함수가 아니라 객체라면 bindActionCreators를 connect에서 대신 해준다
const mapDispatchToProps = {
increase,
decrease,
setDiff
};
// connect 함수에는 mapStateToProps, mapDispatchToProps를 인자로 넣는다
export default connect(
mapStateToProps,
mapDispatchToProps
)(CounterContainer2);
// 위 코드는 다음과 같다
// const enhance = connect(mapStateToProps, mapDispatchToProps);
// export defualt enhance(CounterContainer);
(4) connect 함수 더 깔끔하게 작성하기
mapStateToProps와 mapDispatchToProps를 따로 선언하지 않고 connect함수를 사용할때 인자쪽에서 익명함수로 바로 만들어서 사용하면 코드가 깔끔하다.
예제) TodosContainer.js를 connect 사용해서 구현하기
// connect 사용해서 구현하기
import React, {useCallback} from "react";
import { connect } from "react-redux";
import Todos from "../components/Todos";
import {addTodo, toggleTodo} from '../modules/todos';
function TodosContainer({ todos, addTodo, toggleTodo}) {
const onCreate = text => addTodo(text);
const onToggle = useCallback(id => toggleTodo(id), [toggleTodo]);
// 최적화를 위해 useCallback 사용
return <Todos todos={todos} onCreate={onCreate} onToggle={onToggle} />;
};
export default connect(
state => ({todos: state.todos}),
{
addTodo,
toggleTodo
}
)(TodosContainer);
(5) connect 사용시 알아둬야 하는것들
1. mapStateToProps의 두번째 파라미터 ownProps (생략가능함)
ownProps : 컨테이너 컴포넌트를 렌더링할때 직접 넣어주는 props.
예시)
<CounterContainer myValue={1} />
// {myValue: 1} = ownProps
리덕스에서 어떤 상태를 조회할지 설정하는 과정에서 현재 받아온 props에 따라 다른 상태를 조회할때 사용.
const mapStateToProps = (state, ownProps) => ({
todo: state.todos[ownProps.id]
})
2. connect의 3번째 파라미터 mergeProps(생략가능)
mergeProps : 컴포넌트가 실제로 전달받게 될 props를 정의.
(stateProps, dispatchProps, ownProps) => Object
// 따로 지정하지 않을경우의 결과
// { ...ownProps, ...stateProps, ...dispatchProps }
3. connect의 4번째 파라미터 options(생략가능)
connect 사용시 컨테이너 컴포넌트가 어떻게 동작할지에 대한 옵션을 설정할 수 있음.
Context 커스터마이징, 최적화를 위한 비교 작업 커스터마이징, ref 관련 작업등을 할 수 있다.