1. 개요
첫번째 글에서 흐름을 설명했던 카운터를 실제로 구현해보겠습니다.
코드는 아래 블로그를 참고했습니다.
https://react.vlpt.us/redux/04-make-modules.html
2. 코드 작성
결과물은 위와 같습니다. 인풋 값으로 숫자를 입력받아 그 값만큼 현재값에서 +하거나 -하는 카운터 기능입니다.
프로젝트 폴더 구조는 위와 같습니다.
module/counter.js
/* 액션 타입 만들기 */
// Ducks 패턴을 따를땐 액션의 이름에 접두사를 넣어주세요.
// 이렇게 하면 다른 모듈과 액션 이름이 중복되는 것을 방지 할 수 있습니다.
const SET_DIFF = 'counter/SET_DIFF';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
/* 액션 생성함수 만들기 */
// 액션 생성함수를 만들고 export 키워드를 사용해서 내보내주세요.
export const setDiff = (diff) => ({ type: SET_DIFF, diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
/* 초기 상태 선언 */
const initialState = {
number: 0,
diff: 1
};
/* 리듀서 선언 */
// 리듀서는 export default 로 내보내주세요.
export default function counterReducer(state = initialState, action) {
switch (action.type) {
case SET_DIFF:
return {
...state,
diff: action.diff
};
case INCREASE:
return {
...state,
number: state.number + state.diff
};
case DECREASE:
return {
...state,
number: state.number - state.diff
};
default:
return state;
}
}
modules/reducer.js
import { combineReducers } from "redux";
import counterReducer from "./counter";
const rootReducer = combineReducers({
counterReducer
})
export default rootReducer;
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules/reducer';
const store = createStore(rootReducer);
console.log(store.getState());
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.js
import './App.css';
import CounterContainer from './container/CounterContainer';
function App() {
return (
<div className="App">
<CounterContainer/>
</div>
);
}
export default App;
component/Counter.jsx
import React from 'react';
function Counter({ number, diff, onIncrease, onDecrease, onSetDiff }) {
const onChange = e => {
// e.target.value 의 타입은 문자열이기 때문에 숫자로 변환해주어야 합니다.
onSetDiff(parseInt(e.target.value, 10));
};
return (
<div>
<h1>{number}</h1>
<div>
<input type="number" value={diff} min="1" onChange={onChange} />
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
</div>
</div>
);
}
export default Counter;
container/CounterContainer.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Counter from '../component/Counter';
import { increase, decrease, setDiff } from '../modules/counter';
function CounterContainer() {
// useSelector는 리덕스 스토어의 상태를 조회하는 Hook입니다.
// state의 값은 store.getState() 함수를 호출했을 때 나타나는 결과물과 동일합니다.
const { number, diff } = useSelector(state => ({
number: state.counterReducer.number,
diff: state.counterReducer.diff
}));
// useDispatch 는 리덕스 스토어의 dispatch 를 함수에서 사용 할 수 있게 해주는 Hook 입니다.
const dispatch = useDispatch();
// 각 액션들을 디스패치하는 함수들을 만드세요
const onIncrease = () => dispatch(increase());
const onDecrease = () => dispatch(decrease());
const onSetDiff = (diff) => dispatch(setDiff(diff));
return (
<Counter
// 상태와
number={number}
diff={diff}
// 액션을 디스패치 하는 함수들을 props로 넣어줍니다.
onIncrease={onIncrease}
onDecrease={onDecrease}
onSetDiff={onSetDiff}
/>
);
}
export default CounterContainer;
3. 코드 분석
modules/counter.js를 먼저 보겠습니다. 여기에서는 액션과 액션 생성함수, 리듀서를 지정합니다.
카운터에서 입력받는 숫자가 diff이고, number는 현재 state이며 상태가 변할 값입니다.
만약 number가 현재 state라는 말에 대해 이해가 가지 않는다면 전 글을 읽고 오시길 바랍니다.
https://narup.tistory.com/243?category=1044606
1) 이 counter라는 상태 관리에서는 총 2가지의 state를 관리합니다.
현재 값: number
입력 값: diff
이 state는 리듀서를 통해 store에 저장되고, 저희 개발자는 store에서 이 state를 가져다가 쓸 수 있는데, 자세한 것은 아래에서 후술하겠습니다!
2) increase(), 카운터 증가를 호출할 경우
한 가지 액션이 발생되었을 경우를 예로 들어보겠습니다.
+버튼을 눌렀을 경우, 컴포넌트 측(CounterContainer.jsx)의 dispatch(increase())가 호출되면서 위 코드의 액션 생성 함수가 호출될 것입니다.
그러면 액션 생성함수는 타입이 INCREASE라는 액션을 반환합니다.
export const increase = () => ({ type: INCREASE });
그다음 액션 생성함수가 값을 반환하면, 리듀서로 지정한 counterReducer(state, action)가 호출되고,
그다음 switch-case에 따라
return {
...state,
number: state.number + state.diff
};
...state와 number가 반환됩니다.
...state는 뭐지 싶습니다. ...이 들어간 이 연산자는 스프레드 연산자로 현재 state의 값을 새롭게 복사해서 반환합니다.
Redux에서 불변성을 유지해야 하기 때문에 무조건! 위와 같이 스프레드 연산자를 사용하도록 해야합니다.
이 카운터 리덕스에서는 총 2가지 어떤 상태를 관리한다고 했죠?
number와 diff라는 2가지의 상태를 관리한다고 1번에서 했습니다.
...state는 number와 diff를 그대로 복사해서 return을 해버립니다.
여기서 추가적으로 number가 현재 state의 number와 state의 diff값을 서로 더한 값을 반환한다고 되어있으니,
만약 현재 number가 6이고, diff가 3인 초기 상태였을 경우,
리듀서를 통과하면 현재 number는 9가되고, diff는 3인 되는 것이죠.
counterReducer가 return하면 이 리턴된 값을 스토어(Store)에 저장해야 합니다.
modules/reducer.js에서 combineReducers()함수는 여러 가지 리듀서를 한 가지로 합쳐주는 함수인데,
const rootReducer = combineReducers({
counterReducer, //todoReducer, toggleReducer 등등
})
저희가 프로젝트에서 한 가지 리듀서만 사용하지는 않기 때문에 이처럼 rootReducer라고 리듀서를 따로 지정해서 관리합니다.
그리고 스토어에 리듀서를 저장하기 위해
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
위와 같이 createStore()함수에 rootReducer를 담아 store 객체를 만들고, Provider로 App 컴포넌트를 감싸서 Store를 등록해줍니다.
이렇게 하면 App 컴포넌트 하위에 있는 컴포넌트 들은 프로바이더에 등록된 스토어에서 저장된 값들을 자유롭게 사용할 수 있게 됩니다!
const { number, diff } = useSelector(state => ({
number: state.counterReducer.number,
diff: state.counterReducer.diff
}));
container/CounterContainer.jsx에서처럼 값을 읽을때는 useSelector()를 통해 읽으며,
// useDispatch 는 리덕스 스토어의 dispatch 를 함수에서 사용 할 수 있게 해주는 Hook 입니다.
const dispatch = useDispatch();
// 각 액션들을 디스패치하는 함수들을 만드세요
const onIncrease = () => dispatch(increase());
container/CounterContainer.jsx에서처럼 useDispatch()를 통해 액션 함수를 호출해서 사용합니다.
3) setDiff(), 입력 값을 변경할 경우
한 가지 더 예를 들어보겠습니다.
현재 number는 19이고, diff를 5로 변경하려고 입력했습니다.
위 처럼 Input 컴포넌트에서 숫자를 변경할 경우 디스패치를 통해 setDiff()가 호출됩니다.
2번과 같이 액션 생성함수를 통해 액션 값이 반환되는데,
export const setDiff = (diff) => ({ type: SET_DIFF, diff });
특이한 점은 인자로 diff값을 받아서 액션에 diff값을 담아줍니다. 어떤 값으로 변경할지 지정해주는 것이죠.
그다음 리듀서를 통해
case SET_DIFF:
return {
...state,
diff: action.diff
};
...state, 즉 스프레드 연산자를 통해 현재의 상태(number와 diff)를 반환하며,
action 함수에서 담았던 diff를 diff: action.diff로 초기화해서 반환합니다.
현재 number가 19이고 diff가 1인 상태였다면,
number가 19이고 diff가 5인 상태로 값을 변경해서 반환해주는 것입니다.
2번과 같이 리듀서를 통해서 반환된 값은 스토어(Store)에 저장되며, 각 컴포넌트에서 useSelector()함수를 통해 값을 가져올 수 있게 됩니다.
전체 소스 코드입니다
https://github.com/kimdongjang/learn-redux
'FrontEnd > React 심화' 카테고리의 다른 글
[React] Redux Toolkit 에서의 비동기 처리(createAsyncThunk, extraReducers) (0) | 2022.09.19 |
---|---|
[React] Redux Toolkit 사용법 (1) | 2022.09.19 |
[React] Redux 사용하기(1) - 개념과 흐름 (0) | 2022.06.21 |
[React] Immer (produce) (0) | 2022.04.04 |
[React] Async & Await란? (0) | 2022.03.07 |