๋ฏธ๋ค์จ์ด๋ ์ ์ชฝ์ ์ฐ๊ฒฐํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋๋ก ์ค๊ฐ์์ ๋งค๊ฐ ์ญํ ์ ํ๋ ์ํํธ์จ์ด์ด๋ค.
์ถ์ฒ: ์ํคํผ๋์
๋ฆฌ๋์ค์์ ๋ฏธ๋ค์จ์ด๋ ์ก์ ๊ณผ store ์ค๊ฐ์์ ํน์ ์์ ์ ํด์ฃผ๋ ๋ชจ๋์ด๋ค. ๋ฆฌ๋์ค๋ store๋ฅผ ๋ง๋ค ๋ ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ฉํ ์ ์์ผ๋ฉฐ, redux-saga, redux-thunk ๋ฑ ๋ค์ํ ๋ฏธ๋ค์จ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ์กด์ฌํ๋ค.
์ด ํฌ์คํธ์์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฌ์ฉ๋ณด๋ค๋ ๋ฏธ๋ค์จ์ด ๊ทธ ์์ฒด์ ๋ํด ์์๋ณด๋๋ก ํ์.
// module/counter.js
const INCREMENT = "counter/INCREMENT";
const DECREMENT = "counter/DECREMENT";
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
const initialState = 0;
function counter(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
}
export default counter;
// module/index.js
import { combineReducers } from "redux";
import counter from "./counter";
const rootReducer = combineReducers({ counter });
export default rootReducer;
import React, { useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { increment } from "./module/counter";
const App = () => {
const count = useSelector((state) => state.counter);
const dispatch = useDispatch();
return (
<>
<div>{count}</div>
<button onClick={() => dispatch(increment())}>+</button>
</>
);
};
export default App;
// index.js
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ์์ App ์ปดํฌ๋ํธ๋ react-redux ํจํค์ง๊ฐ ์ ๊ณตํ๋ hook์ ํตํด counter state์ ์ ๊ทผํ๊ณ ์๋ค.
์ฌ๊ธฐ์ console์ ํตํด ๋ก๊น ํ๋ ๋ฏธ๋ค์จ์ด๋ฅผ ์์ฑํด๋ณด์.
// middleware.js
export function loggerMiddleware(store) {
return function (next) {
return function (action) {
console.log(store.getState());
console.log(action);
next(action);
};
};
}
๋ฆฌ๋์ค์์ ๋ฏธ๋ค์จ์ด์ ๊ท๊ฒฉ์ store๋ฅผ ๋ฐ์ next๋ฅผ ์ธ์๋ก ๋ฐ๋ ํจ์๋ฅผ ๋ฆฌํดํ๊ณ , next๋ฅผ ์ธ์๋ก ๋ฐ๋ ํจ์์์ action์ ์ธ์๋ก ๋ฐ๋ ํจ์๋ฅผ ๋ฆฌํดํ๋ค.
export const loggerMiddleware = (store) => (next) => (action) => {
console.log(store.getState());
console.log(action);
next(action);
}
๋ค์๊ณผ ๊ฐ์ด ํ์ดํ ํจ์๋ฅผ ์ด์ฉํด์ ์์ฑํ ์๋ ์๊ฒ ๋ค.
์ด๋ ๊ฒ ์์ฑํ๋ ์ด์ ๋ ๋ฆฌ๋์ค ๋ด๋ถ์ applyMiddleware๋ฅผ ํตํด ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ฉ์ํค๊ธฐ ๋๋ฌธ์ด๋ค.
ํด๋น ๋ด๋ถ์ ๊ตฌํ ์ฌํญ์ด ์์ ๊ฐ์ ํํ๋ฅผ ๋ฐ๋ฅด๊ธฐ ๋๋ฌธ์ ๋ฏธ๋ค์จ์ด๋ฅผ ์์ฑํ ๋์๋ ์์ ๊ฐ์ currying ํจํด์ ์ด์ฉํด์ผํ๋ค.
์ด๋ ๊ฒ ํ๋ฉด ๋งค๋ฒ ํจ์ ํธ์ถ ์ store์ next๋ฅผ ์ ๋ฌํ์ง ์๊ณ action๋ง ์ ๋ฌ๋ฐ๋ ํจ์๋ฅผ ์ฌ์ฉํ ์ ์๋ค๋ ์ฅ์ ์ด ์๋ค.
store๋ ๊ณ ์ ์ด๊ณ , next๋ ๋ค์ ๋ฏธ๋ค์จ์ด, ๋ง์ง๋ง์ด๋ผ๋ฉด reducer์ด๊ธฐ ๋๋ฌธ์ ๊ณ ์ ๊ฐ์ด๋ค. ๋ณํํ๋ ๋์์ธ action์๋ง ๊ด์ฌ์ฌ๋ฅผ ๋ ์ ์๋ค๋ ๊ฒ๋ ์ฅ์ ์ด๋ค.
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
index.js ํ์ผ์์ ๋ค์๊ณผ ๊ฐ์ ํํ๋ก ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ฉ์์ผฐ๋ค. ์ฝ์ ์ฐฝ์ ํตํด ์ ๋ฌ๋ฐ์ ํ์ฌ store์ state์ ์ ๋ฌ๋ฐ์ ์ก์ ์ด ๋ฌด์์ธ์ง๋ฅผ ํ์ ํ ์ ์๋ค!
๋ฆฌ๋์ค๋ ๋๊ธฐ์ ์ผ๋ก ์๋ํ๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ํด์ ๋ฐ๋ก ์ถ๊ฐ์ ์ธ ์ฒ๋ฆฌ๊ฐ ํ์ํ๋ค. ์์ํ๋ฏ์ด ์ด ๋ํ ๋ฏธ๋ค์จ์ด๋ฅผ ์ด์ฉํด ์ฒ๋ฆฌ ๊ฐ๋ฅํ๋ฉฐ ์์๋ ๋ค์๊ณผ ๊ฐ๋ค.
// module/board.js
const FETCH_START = "board/fetch_start";
const FETCH_FAILED = "board/fetch_failed";
const FETCH_SUCCESS = "board/fetch_success";
const initialState = {
loading: false,
boards: [],
};
export const createRequestThunk = (id) => async (dispatch, getState) => {
dispatch({ type: FETCH_START });
try {
const result = await fetch(
`https://jsonplaceholder.typicode.com/todos/${id + 1}`
).then((res) => res.json());
dispatch({ type: FETCH_SUCCESS, payload: result });
} catch (e) {
console.log(e);
dispatch({ type: FETCH_FAILED });
}
};
function board(state = initialState, action) {
switch (action.type) {
case FETCH_START:
return { ...state, loading: true };
case FETCH_SUCCESS:
return {
...state,
boards: state.boards.concat(action.payload),
loading: false,
};
case FETCH_FAILED:
return {
...state,
loading: false,
};
default:
return state;
}
}
export default board;
์ถ๊ฐ์ ์ธ ์์ ์ ์ํด board๋ผ๋ ์ด๋ฆ์ผ๋ก ๋ฆฌ๋์๋ฅผ ํ๋ ์์ฑํ๋ค. ์ด ๋ฆฌ๋์๋ฅผ ๊ธฐ์กด rootReducer์ ์ฐ๊ฒฐํด์ผํ๋ค.
์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด createRequestThunk๊ฐ ๋ณด์ธ๋ค. createRequestThunk๋ ํจ์๋ฅผ ๋ฐํํ๋ ํจ์, ์ฆ ๊ณ ์ฐจํจ์์ด๋ค. ์ฌ์ฌ์ฉ์ฑ์ ์ํด์ ๋ฐ๋ก ๊ด๋ฆฌํ๋ ๊ฒ ์ข์ง๋ง ์์๋ฅผ ์ํด ํ ํ์ผ์ ์์ฑํ๋ค.
createRequestThunk๋ฅผ ํตํด ๋ฐํ๋ฐ์ ํจ์๊ฐ ํ๋ ์ผ์ ๊ฐ๋จํ๋ค.
์ฌ์ฉ์ App ์ปดํฌ๋ํธ์์ ํ๋ค.
onst App = () => {
const count = useSelector((state) => state.counter);
const boards = useSelector((state) => state.board.boards);
const loading = useSelector((state) => state.board.loading);
const dispatch = useDispatch();
const fetchData = () => {
dispatch(increment());
dispatch(createRequestThunk(count));
};
return (
<>
<div>{count}</div>
<button onClick={fetchData}>+</button>
{loading ? (
<span>Loading...</span>
) : (
boards.map((item) => <div>{item.title}</div>)
)}
</>
);
};
export default App;
fetchData ํจ์๋ฅผ ๋ณด๋ฉด ๋ ๋ฒ์งธ dispatch๋ก ํจ์๊ฐ ๋ค์ด๊ฐ๋ค. dispatch์ ์ ๋ฌํ๋ ์ก์ ์ plain object์ฌ์ผ ํ๋๋ฐ ์ด๊ฒ์ ๊ท์น์ ์ด๊ธ๋๋ค.
์ด๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด์ ๋ฏธ๋ค์จ์ด๋ฅผ ํ์ฉํ ์ ์๋ค.
// middleware.js
...
export function thunkMiddleware(store) {
return function (next) {
return function (action) {
if (typeof action === "function") {
action(store.dispatch, store.getState);
} else {
next(action);
}
};
};
}
์ด ๋ฏธ๋ค์จ์ด๊ฐ ํ๋ ์ญํ ์ ์ก์ ์ด ํจ์ ํ์ ์ผ๋ก ๋ค์ด์ฌ ๋์ ๊ทธ๋ ์ง ์์ ๋๋ฅผ ๊ตฌ๋ถํด์ ๊ฐ๊ธฐ ๋ค๋ฅธ ์์ ์ ์ํํ๋ ๊ฒ์ด๋ค.
ํจ์ ํ์ ์ด ์๋ ๋ ํ๋ฒํ๊ฒ next ํธ์ถ์ ํ์ง๋ง ํจ์ ํ์ ์ผ ๊ฒฝ์ฐ ํด๋น ํจ์๋ฅผ ์คํ์์ผ๋ฒ๋ฆฐ๋ค.
์ด๋ createRequestThunk์ store.dispatch์ store.getState๋ฅผ ์ ํด ๋ฆฌ๋์ค์ ๊ธฐ๋ฅ์ ์ ๊ทผํ ์ ์๊ฒ ํ๋ค.
createRequestThunk์ ๊ฒฝ์ฐ ๋ด๋ถ์์ dispatch๋ฅผ ์ํํ๊ธฐ ๋๋ฌธ์ ์ด๊ฒ์ ๋ค์ ๋ฏธ๋ค์จ์ด๋ก ๋ค์ด๊ฐ๊ณ ์ ๋ฌํ ์ก์ ์ด plain object์ด๊ธฐ ๋๋ฌธ์ ๋ณ๋ค๋ฅธ ์ฒ๋ฆฌ ์์ด next์ action์ ์ ๋ฌํด ํธ์ถํ๋ค.
redux thunk ํจํค์ง๊ฐ ํด์ฃผ๋ ์ผ์ด ์์ ๊ฐ์ ์์ ์ด๋ค. ๋ฌผ๋ก ๋ด๋ถ์ ์ผ๋ก ๋ ์ ๊ตํ ์์ ์ ์คํํ๊ฒ ์ง๋ง ์ ์ฒด์ ์ธ ๋ชจ์ต์ ์์ ์์ฑํ ์์์ ๊ฐ๋ค.
๋๊ธฐ์ ์ผ๋ก ์๋ํ๋ ๋ฆฌ๋์ค์ ์ก์ ๋ค์ ํ ๋ฉ์ด๋ฆฌ๋ก ๋ฌถ์ด ๋น๋๊ธฐ ์์ ์ ์ํํ๋๋ก ํด์ฃผ๋ ๊ฒ์ด๋ค.
๋ฏธ๋ค์จ์ด์ ๋ํด ์ฌ์ฉํ๊ณ ๋ ์์์ง๋ง ์ด๋ฐ ๋ฐฉ์์ผ๋ก ๋์๊ฐ๋์ง๋ ์์ง๋ ๋ชปํ๋ค. ์ต๊ทผ์๋ ๋ฆฌ๋์ค ํดํท๋ ์๊ธฐ๊ณ ์ด๋ ค์์ด ์ ์ ์ฌ๋ผ์ง๊ณ ์๋ค์ง๋ง, ๊ทธ๋๋ ์๋ฆฌ์ ์ธ ๋ถ๋ถ์ ์ดํดํ๋ ๊ฒ๋ ์ค์ํ๋ค๊ณ ์๊ฐ๋๋ค.