안녕하세요
Redux 두번째 포스팅입니다.
지난 번 포스팅에서는 View를 담당하는 React를 Redux와 결합하기위해서 store객체의 subscribe 메서드를 사용했습니다.
store.subscribe(()=> callback)
store객체의 데이터가 변경되면 subscribe메서드로 등록한 콜백함수가 동작하므로 콜백함수로 컴포넌트가 업데이트 되도록 하면 됐었는데요
위 그림에서 Store --> View(React)의 과정이었습니다.
아무래도 Redux와 React를 결합하기위해 직접 손으로 일일이 subscribe를 이용하는 것보단,
react-redux
처럼 결합시켜주는 라이브러리를 이용하는게 좋습니다.
또, 단순하게 결합만 시켜주는 것이 아니라 다른 장점들도 가지고 있습니다.
아무튼 그래서 오늘은 react-redux
라이브러리를 사용하는 방법에 대해 알아보고, store를 불변객체로 관리하기위한 immer
, 그리고 Redux의 로직에 관여할 수 있는 Middleware를 사용하는 방법에 대해 정리해보겠습니다.
npm i react-redux
설치를 마쳤다면, ContextAPI를 사용할 때와 마찬가지로 상위컴포넌트에서 하위컴포넌트들을 Provider 컴포넌트로 감싸줍니다. 이 때 속성값으로 store객체를 넣어줍니다.
Provider컴포넌트는 전달받은 Store객체의 subscribe 메서드를 호출해서 액션 처리가 끝났을 때마다 알림을 받습니다. 그 다음 ContextAPI를 사용해서 리덕스의 상탯값을 하위 컴포넌트로 전달합니다.
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store'
import App from './App'
const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
Redux는 애플리케이션의 전체 상탯값을 하나의 객체로 관리한다고 했습니다. 애플리케이션이 커진다면 당연히 객체의 크기도 커집니다.
객체의 모든 상태값을 각 컴포넌트 마다 모두 전달한다면 비효율적입니다.
connect() 함수는 컴포넌트에 필요한 상탯값과 Dispatch 함수를 속성값으로 전달합니다.
connect( mapStateToProps, mapDispatchToProps )
두 인자는 모두 함수입니다.
두 인자를 작성하는 방법을 살펴보겠습니다.
이 함수의 리턴값이 컴포넌트의 속성으로 전달되게 됩니다.
필요한 값만 전달해야 불필요한 렌더링이 없습니다.
function mapStateToProps(state) {
const { todos } = state
return { todoList: todos.allIds }
}
export default connect(mapStateToProps)(TodoList)
이렇게 전달한다면 컴포넌트 내에서 counter라는 이름으로 속성값을 사용할 수 있습니다.
state는 Store의 전체 상탯값이며 store.getState()
로 호출해서 확인할 수 있습니다.
subscribe메서드를 호출하고 싶지 않다면 (컴포넌트의 자동업데이트), mapStateToProps
메서드 대신에 null 또는 undefined를 넣어주면 됩니다.
export default connect(null, mapDispatchToProps)(Component)
생략해도 되지만, 부모 컴포넌트에서 전달하는 속성값을 핸들링 하고싶은 경우 선택적으로 사용할 수 있습니다.
// Todo.js
function mapStateToProps(state, ownProps) {
const { visibilityFilter } = state
const { id } = ownProps
const todo = getTodoById(state, id)
// component receives additionally:
return { todo, visibilityFilter }
}
// Later, in your application, a parent component renders:
// <ConnectedTodo id={123} />
위 코드에서는 부모 컴포넌트에서 전달한 id를 이용해 getTodoById라는 메서드를 호출하고 todo라는 이름의 속성값을 생성해 전달했습니다.
리턴값은 객체로 표현됩니다. 공식문서에 보면 Plain Object를 리턴해야 한다고 합니다.
// Plain Object
var plainObj1 = {}; // typeof plainObj1 --> Object
var plainObj2 = {name : "myName"}; // typeof plainObj2 --> Object
var plainObj3 = new Object(); // typeof plainObj3 --> Object
일반적으로 Class같은 것들을 사용해 리턴하지 않기 때문에 return에서 만큼은 크게 주의할 것은 없는 것 같습니다.
mapStateToProps 메서드는 가능한 한 가볍게 작성해야합니다.
또, 순수함수로 작성해야하며 동기적으로 실행되야합니다.
Ajax같은 비동기 통신을 여기에 작성하면 안됩니다.
다음은 connect()함수의 두 번째 인자 mapDispatchToProps
입니다.
connect 메서드의 두 번째 인수 함수인 mapDispatchToProps
입니다.
리덕스 store의 dispatch를 속성값으로 전달할 때 사용됩니다.
즉 Store의 상태를 변경할 함수를 컴포넌트의 속성값으로 전달해줍니다.
connect(null, null)(MyComponent)
처럼 mapDispatchToProps를 지정하지 않는다면 props.dispatch
로 dispatch에 접근할 수 있습니다.
앞서 Provider 컴포넌트의 속성값으로 store를 ContextAPI를 통해 전달하고 있기 때문에 기본적인 사용이 가능한 것입니다.
만약 increment()
라고 하는 dispatch 함수를 전달하려고 한다면 컴포넌트 내부에서의 접근방법은 props.dispatch(()=>increment())
가 될것입니다.
반대로 mapDispatchToProps를 이용한다면 increment라는 dispatcher를 특정해 사용할 수 있으며 props.increment()
로 직접 속성값으로 참조할 수 있게됩니다.
이렇게 캡슐화 과정을 거치면 React에서는 Redux의 존재를 모르고 컴포넌트를 작성할 수 있게 됩니다.
import { connect } from 'react-redux'
import { increment, decrement, reset } from './actionCreators'
// const Component = ...
const mapStateToProps = (state /*, ownProps*/) => {
return {
counter: state.counter
}
}
const mapDispatchToProps = { increment, decrement, reset }
export default connect(
mapStateToProps,
mapDispatchToProps
)(Component)
함수로 정의하는 것과 객체로 정의하는 법 두 가지 방법이 있는데 먼저 함수로 정의하는 방법부터 보겠습니다.
const mapDispatchToProps = dispatch => {
return {
// dispatching plain actions
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
reset: () => dispatch({ type: 'RESET' })
}
}
const mapDispatchToProps = dispatch => {
return {
// explicitly forwarding arguments
onClick: event => dispatch(trackClick(event)),
// implicitly forwarding arguments
onReceiveImpressions: (...impressions) =>
dispatch(trackImpressions(impressions))
}
}
아래 코드처럼 객체 형태로 사용할 수도 있습니다.
import {increment, decrement, reset} from "./counterActions";
const actionCreators = {
increment,
decrement,
reset
}
export default connect(mapState, actionCreators)(Counter);
/* or
export default connect(
mapState, { increment, decrement, reset }
)(Counter);
*/