+ 알아야 할것
redux, react-redux
redux 의 useSelector 훅과 dispatch 훅
+ 실습
App 컴포넌트에 players state를 redux의 store로 이동
store로 부터 players 데이터를 subscription하도록 구조 변경
자식이 부모와 통신하기 위해서 콜백 함수를 props로 내려받는 구조를 액션 dispatch로 변경
redux 구조 적용
redux 기본 구조를 만든다. 먼저 redux tutorial을 선행학습을 해야만 된다.
구조는 다음과 같다.
redux
+ reducers
– index.js
– players.js
– actions.js
– actionTypes.js
– store.js
redux/reducers/players.js
1 2 3 4 5 6 7 8 9 |
const playerInitialState = { players: [], }; export const playerReducer = (state = playerInitialState, action) => { switch(action.type) { default: return state; } } |
redux/reducers/index.js
1 2 3 |
import {combineReducers} from "redux"; import {playerReducer} from "./players"; export const allReducers = combineReducers({playerReducer}); |
redux/store.js
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.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import {Provider} from "react-redux"; import {store} from "./redux/store"; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); |
App.jsx – players 데이터 받기
playerReducer에 players 데이터를 정의하였으므로 usetState는 삭제하고 이것을 가져와야 한다(L3)
useState를 삭제하였으므로 setPlayrs 부분을 모두 주석 처리해야 에러가 나지 않을것이다(L11, L21, L35, L47).
여기까지하고 테스트하면 화면에 players는 나타나지 않을것이다. store에 빈 배열의 players를 가져왔기 때문에 정상이다.
App.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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
const App = () => { // 전체 store에서 playerReducer 에서 사용중인 players 데이터를 가져온다. const players = useSelector(state => state.playerReducer.players); const dispatch = useDispatch(); useEffect(() => { axios.get('http://api.eastflag.co.kr:8000/api/score/list') .then(response => { console.log(response); const {data} = response; // setPlayers(data); }); }, []) const handleRemovePlayer = (id) => { axios.delete(`http://api.eastflag.co.kr:8000/api/score?id=${id}`) .then(response => { console.log(response); const {data} = response; if (data.result === 0) { // setPlayers(players.filter(item => item.id !== id)); } }); } const handleChangeScore = (id, delta) => { console.log('id: ' + id, 'delta: ' + delta); const copyPlayers = [ ...players ]; copyPlayers.forEach(player => { if (player.id === id) { player.score += delta; } }) // setPlayers(copyPlayers); } const handleAddPlayer = (name) => { console.log(name); axios.post('http://api.eastflag.co.kr:8000/api/score', {name}) .then(response => { console.log(response); const {data} = response; const copyPlayers = [ ... players ]; copyPlayers.unshift(data); // setPlayers(copyPlayers); }); }; const getHighScore = () => { const maxObject = _.maxBy(players, 'score'); const highScore = maxObject.score; // 0은 디폴트이므로 0보다 클 경우만 highScore로 지정한다. return highScore > 0 ? highScore : null; } return ( <div className="container p-3"> <Header title="My scoreboard" players={players} /> {/*Players List*/} { players.map(item => <CustomPlayer key={item.id} name={item.name} score={item.score} id={item.id} isHighScore={item.score === getHighScore()} removePlayer={handleRemovePlayer} changeScore={handleChangeScore} />) } <AddPlayerForm addPlayer={handleAddPlayer}></AddPlayerForm> </div> ) } |
App.jsx – 초기 데이터 dispatch
액션타입 정의
redux/actionTypes.js
1 |
export const SET_PLAYER = 'player/SET_PLAYER'; |
액션 creator 정의
redux/actions.js
1 2 3 4 5 6 |
export const setPlayer = (players) => { return { type: SET_PLAYER, players } } |
reducer 로직 작성
redux/reducers/players.js
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export const playerReducer = (state = playerInitialState, action) => { let copyPlayers; switch(action.type) { case SET_PLAYER: return { ...state, players: action.players } default: return state; } } |
액션 디스패치
App.jsx
1 2 3 4 5 6 7 8 9 |
const dispatch = useDispatch(); useEffect(() => { axios.get('http://api.eastflag.co.kr:8000/api/score/list') .then(response => { console.log(response); const {data} = response; dispatch(setPlayer(data)); }); }, []) |
화면에 players가 나타남을 확인한다. 크롬 디버거 탭 redux에서 액션이 디스패치되는지도 확인한다.
App.jsx – player 추가 dispatch
액션타입 정의
redux/actionTypes.js
1 |
export const ADD_PLAYER = 'player/ADD_PLAYER'; |
액션 creator 정의
redux/actions.js
1 2 3 4 5 6 |
export const addPlayer = ({id, name, score}) => { return { type: ADD_PLAYER, player: {id, name, score} } } |
reducer 로직 작성
redux/reducers/players.js
1 2 3 4 5 6 7 8 |
case ADD_PLAYER: copyPlayers = [ ...state.players ]; copyPlayers.unshift(action.player); return { ...state, players: copyPlayers } |
App -> AddPlayerForm 으로 함수를 전달해서 부모와 자식간의 통신을 했는데 이제 AddPlayerForm 에서 직접 액션을 dispatch할 수 있으므로 App 컴포넌트에서 AddPlayerForm으로 내려보낸 addPlayer props를 삭제한다(L13). 또한 handleAddPlayer 함수도 삭제한다.
App.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 |
return ( <div className="container p-3"> <Header title="My scoreboard" players={players} /> {/*Players List*/} { players.map(item => <CustomPlayer key={item.id} name={item.name} score={item.score} id={item.id} isHighScore={item.score === getHighScore()} removePlayer={handleRemovePlayer} changeScore={handleChangeScore} />) } <AddPlayerForm></AddPlayerForm> </div> ) |
AddPlayerForm 에서는 부모로부터 받은 props 를 통해서 함수를 호출하는게 아니라 store에 직접 dispatch 한다. Promise 패턴은 async-await 패턴으로 바꾸었다.
components/AddPlayerForm.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const dispatch = useDispatch(); ... const handleSubmit = async (e) => { e.preventDefault(); const form = formRef.current; // form node const player = textRef.current; // input node console.log(player.validity.valid); console.log(form.checkValidity()); if (!form.checkValidity()) { form.classList.add('was-validated'); return; } const response = await axios.post('http://api.eastflag.co.kr:8000/api/score', {name: value}) console.log(response); const {data} = response; dispatch(addPlayer(data)) setValue('') form.classList.remove('was-validated'); } |
player 추가가 되는지 확인한다. 크롬 디버거 탭 redux에서 액션이 디스패치되는지도 확인한다.
App.jsx – player 삭제 dispatch
액션타입 정의
redux/actionTypes.js
1 |
export const REMOVE_PLAYER = 'player/REMOVE_PLAYER'; |
액션 creator 정의
redux/actions.js
1 2 3 4 5 6 |
export const removePlayer = (id) => { return { type: REMOVE_PLAYER, id } } |
reducer 로직 작성
redux/reducers/players.js
1 2 3 4 5 6 |
case REMOVE_PLAYER: const players = state.players.filter(item => item.id !== action.id) return { ...state, players } |
App -> CustomPlayer -> Player 으로 함수를 전달해서 부모와 자식간의 통신을 했는데 이제 Player 에서 직접 액션을 dispatch할 수 있으므로 App 컴포넌트에서 CustomPlayer으로 내려보낸 addPlayer props를 삭제한다. 또한 handleAddPlayer 함수도 삭제한다.
App.jsx
1 2 3 4 5 6 7 8 9 10 11 12 |
return ( <div className="container p-3"> <Header title="My scoreboard" players={players} /> {/*Players List*/} { players.map(item => <CustomPlayer key={item.id} name={item.name} score={item.score} id={item.id} isHighScore={item.score === getHighScore()} changeScore={handleChangeScore} />) } <AddPlayerForm></AddPlayerForm> </div> ) |
Player 컴포넌트에서 삭제하는 액션을 직접 디스패치한다(L9)
components/Player.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
export const Player = (props) => { const dispatch = useDispatch(); return ( <div className="container"> <div className='player row align-items-center'> <div className="col-1"> <button className="btn btn-danger" onClick={() => dispatch(removePlayer(props.id))}>x</button> </div> <div className="col-8"> {props.children} <span>{props.name}</span> </div> <div className="col-3 counter"> <Counter score={props.score} id={props.id} changeScore={props.changeScore} /> </div> </div> </div> ) } |
player 삭제가 되는지 확인한다. 크롬 디버거 탭 redux에서 액션이 디스패치되는지도 확인한다.
App.jsx – handleScore dispatch
액션타입 정의
redux/actionTypes.js
1 |
export const CHANGE_SCORE = 'player/CHANGE_SCORE'; |
액션 creator 정의
redux/actions.js
1 2 3 4 5 6 7 |
export const changeScore = (id, delta) => { return { type: CHANGE_SCORE, id, delta } } |
reducer 로직 작성
redux/reducers/players.js
1 2 3 4 5 6 7 8 9 10 11 |
case CHANGE_SCORE: copyPlayers = [ ...state.players ]; copyPlayers.forEach(player => { if (player.id === action.id) { player.score += action.delta; } }) return { ...state, players: copyPlayers } |
App -> CustomPlayer -> Player -> Counter으로 함수를 전달해서 부모와 자식간의 통신을 했는데 이제 Counter 에서 직접 액션을 dispatch할 수 있으므로 App 컴포넌트에서 CustomPlayer으로 내려보낸 changeScore props를 삭제한다. 또한 handleChangeScore 함수도 삭제한다.
App.jsx
1 2 3 4 5 6 7 8 9 10 11 12 |
return ( <div className="container p-3"> <Header title="My scoreboard" players={players} /> {/*Players List*/} { players.map(item => <CustomPlayer key={item.id} name={item.name} score={item.score} id={item.id} isHighScore={item.score === getHighScore()} />) } <AddPlayerForm></AddPlayerForm> </div> ) |
Counter 컴포넌트에서 액션을 직접 디스패치한다.
components/Counter.jsx
1 2 3 4 5 6 7 8 9 10 |
export const Counter = (props) => { const dispatch = useDispatch(); return ( <div className='d-flex justify-content-between align-items-center'> <button className='btn btn-info' onClick={() => dispatch(changeScore(props.id, -1))}> - </button> <span>{props.score}</span> <button className='btn btn-info' onClick={() => dispatch(changeScore(props.id, 1))}> + </button> </div> ); } |
score가 바뀌는지 확인한다. 크롬 디버거 탭 redux에서 액션이 디스패치되는지도 확인한다.