지금까지는 redux 튜토리얼이었다. 이제 redux에서 생성한 store를 리액트에 어떻게 주입하고 subscribe 와 publish (action dispatch)를 리액트에서 어떻게 하는지 살펴보자.
먼저 redux를 react에 적용하도록 도와주는 react-redux 라이브러리를 설치한다.
1 |
npm install --save react-redux |
Provider
생성된 store를 리액트에 주입하기 위해서 Provider를 사용한다.
1 2 3 4 |
ReactDOM.render( <provider store="{store}"><app></app></provider>, document.getElementById('root') ); |
모듈화
하나의 화일에 만든 redux 구조를 패턴화 한다.
+ Q: ducks 패턴이란?
ducks 패턴이란 action type 과 action creator 그리고 reducer를 모두 하나의 화일에 보관하는것을 말한다. OOP의 Single Responsibility 원칙에 어긋나는것 처럼 보인다. 하지만 action type과 action creator는 reducer에서만 사용되므로 셋을 합쳐서 하나의 SRP라고 생각하자.
+ redux/reducers/userReducer.js 생성
index.js에 있는 userReducer를 잘라내서 해당화일로 가져온다.
그리고, 액션 type과 action creator를 모두 정의한다. action은 객체이고 action type은 스트링이다. action creator는 action을 만드는 함수이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// ducks 패턴: 액션타입, 액션크리에이터, 리듀서가 모두 동일한 화일에 존재 // action type: 스트링, 모듈명/액션타입으로 표시 export const UPDATE_USER = 'user/UPDATE_USER'; // action creator: 액션을 동적으로 생성하는 펑션 export const updateUser = (name) => ({ type: UPDATE_USER, payload: name }); const initialState = { name: 'Tom' }; export const userReducer = (state = initialState, action) => { switch(action.type) { case UPDATE_USER: return { ...state, name: action.payload }; default: return state; } } |
+ redux/reducers/counterReducer.js
index.js에 있는 counterReducer를 잘라내서 해당화일로 가져온다. 마찬가지로 action type과 action creator를 생성한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// ducks 패턴: 액션타입, 액션크리에이터, 리듀서가 모두 동일한 화일에 존재 // action type: 스트링, 모듈명/액션타입으로 표시 export const INCREASE = 'counter/INCREASE'; export const DECREASE = 'counter/DECREASE'; // action creator: 액션을 동적으로 생성하는 펑션 export const increaseCount = () => ({ type: INCREASE }); export const decreaseCount = () => ({ type: DECREASE }); const initialState = 0; export const counterReducer = (state = initialState, action) => { switch(action.type) { case INCREASE: return state + 1; case DECREASE: return state - 1; default: return state; } } |
+ redux/reducers/index.js : index.js에 있는 allReducers를 잘라내서 해당화일로 가져온다.
1 2 3 4 5 6 7 8 |
import {combineReducers} from "redux"; import {userReducer} from "./userReducer"; import {counterReducer} from "./counterReducer"; export const allReducer = combineReducers({ count: counterReducer, user: userReducer }) |
+ redux/store.js : index.js에서 store 생성부분을 잘라서 가져온다
1 2 3 4 |
import {createStore} from "redux"; import {allReducers} from "./reducers"; export const store = createStore(allReducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); |
+ index.js : 모듈화하고 남은 index.js
1 2 3 4 5 6 7 8 |
import {Provider} from "react-redux"; import {store} from "./redux/store"; import {updateUser} from "./redux/actions"; ReactDOM.render( <provider store="{store}"><app></app></provider>, document.getElementById('root') ); |
count state 변경 및 바인딩 테스트
+ component/Counter.jsx 생성
store의 count 상태를 변경하고 그리고 값을 가져와서 바인딩하는 테스트를 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import React from 'react'; function Counter({count, onIncrease, onDecrease}) { return ( <div> <button onClick={onDecrease}>-</button> <span>{count}</span> <button onClick={onIncrease}>+</button> </div> ); } export default Counter; |
App 컴포넌트에는 자식 컴포넌트인 Counter.jsx 컴포넌트를 포함한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function App(props) { return ( <div className="App"> <Counter count={props.count} onIncrease={props.onIncrease} onDecrease={props.onDecrease}></Counter> </div> ); } // subscribe: 스토어의 state를 props로 매핑하기 const mapStateToProps = (state) => ({ // 왼쪽은 props, 오른쪽은 store의 state count: state.count }); // publish: (액션을 디스패치하는) 펑션을 props 로 매핑하기 const mapActionToProps = (dispatch) => ({ // 왼쪽은 props, 오른쪽은 (액션을 디스패치하는) 펑션 onIncrease: () => dispatch(increaseCount()), onDecrease: () => dispatch(decreaseCount()) }) // 문법적으로는 파라메터를 차례대로 넣는 커링 펑션 // 결과적으로 만드는것은 App을 입력으로해서 새로운 HoC 컴포넌트를 만든다. export default connect(mapStateToProps, mapActionToProps)(App); |
user state 변경 및 바인딩 테스트
+ component/User.jsx 생성
store의 user 상태를 변경하고 그리고 값을 가져와서 바인딩하는 테스트를 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import React from 'react'; function User({name, updateUser}) { return ( <div> <p>{name}</p> <input type="text" id="user_name" /> <button onClick={() => (updateUser(document.getElementById('user_name').value))}>유저 이름 변경</button> </div> ); } export default User; |
App 컴포넌트에 자식 컴포넌트인 User.jsx 컴포넌트를 포함한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
function App(props) { return ( <div className="App"> <Counter count={props.count} onIncrease={props.onIncrease} onDecrease={props.onDecrease}></Counter> <User name={props.user.name} updateUser={props.updateUser}></User> </div> ); } // subscribe: 스토어의 state를 props로 매핑하기 const mapStateToProps = (state) => ({ // 왼쪽은 props, 오른쪽은 store의 state count: state.count, user: state.user }); // publish: (액션을 디스패치하는) 펑션을 props 로 매핑하기 const mapActionToProps = (dispatch) => ({ // 왼쪽은 props, 오른쪽은 (액션을 디스패치하는) 펑션 onIncrease: () => dispatch(increaseCount()), onDecrease: () => dispatch(decreaseCount()), updateUser: (name) => dispatch(updateUser(name)) }) // 문법적으로는 파라메터를 차례대로 넣는 커링 펑션 // 결과적으로 만드는것은 App을 입력으로해서 새로운 HoC 컴포넌트를 만든다. export default connect(mapStateToProps, mapActionToProps)(App); |
redux hooks 적용하기
react-redux 7.1.0 이후부터 react redux hooks가 도입되었다.
connect 로 HoC 컴포넌트를 만든후 component 트리를 보게 되면 많은 래퍼 컴포넌트가 중첩이 된것을 볼 수 있는데, 이제 그런 래퍼 컴포넌트 중첩없이 간단하게 사용할 수 있게 되었다.
useSelector가 connect 펑션의 첫번째 파라메터를 대체하고,
useDispatch가 connect 펑션의 두번째 파라메터를 대체한다.
그리고 connect 하는 더이상의 HoC 래퍼 컴포넌트는 없다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function App() { const count = useSelector(state => state.count); const user = useSelector(state => state.user); const dispatch = useDispatch(); return ( <div className="App"> <Counter count={count} onIncrease={() => dispatch(increaseCount())} onDecrease={() => dispatch(decreaseCount())}></Counter> <User name={user.name} updateUser={(name) => dispatch(updateUser(name))}></User> </div> ); } export default App; |