๐ Redux ๋ฏธ๋ค์จ์ด
๋ฆฌ๋์ค์์ dispatchํ๋ฉด action์ด ๋ฆฌ๋์๋ก ์ ๋ฌ์ด ๋๊ณ ๋ฆฌ๋์๋ ์๋ก์ด state๋ฅผ ๋ฐํํ๋ค. ์ด๋ ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๋ฉด ์ด ๊ณผ์ ์ฌ์ด์ ์ฐ๋ฆฌ๊ฐ ํ๊ณ ์ถ์ ์์ ๋ค์ ๋ฃ์ ์ ์๋ค. ์ฆ ๋ฆฌ๋์ค ๋ฏธ๋ค์จ์ด๋ ์๋ฒ์์ ํต์ ์ ์ํด์ ์ฌ์ฉํ๋ ๊ฒ์ด๋ผ๊ณ ํ ์ ์๋ค.
๐ Thunk
๋ฆฌ๋์ค Thunk๋? ๋ฆฌ๋์ค์์ ๋ง์ด ์ฌ์ฉํ๊ณ ์๋ ๋ฏธ๋ค์จ์ด ์ค ํ๋์ด๋ค. thunk๋ฅผ ์ฌ์ฉํ๋ฉด dispatch๋ฅผ ํ ๋ ๊ฐ์ฒด๊ฐ ์๋ ํจ์๋ฅผ dispatch ํ ์ ์๊ฒ ํด์ค๋ค.
๐ Thunk ์ฌ์ฉํ๊ธฐ
1) Redux ToolKit์ ์ฌ์ฉํ ํ๋ก์ ํธ ์์ฑ
2) Thunk ํจ์ ๋ง๋ค๊ธฐ
3) extraReducer์ thunk ๋ฑ๋กํ๊ธฐ
4) dispatch(thunk ํจ์) ํ๊ธฐ
ํด๋๊ตฌ์กฐ
configStore.js
import counter from "../modules/counterSlice";
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: {
counter,
},
});
export default store;
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import store from "./redux/config/configStore";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
reportWebVitals();
counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
number: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducer: {
addNumber: (state, action) => {
state.number = state.number + action.payload;
},
minusNumber: (state, action) => {
state.number = state.number - action.payload;
},
},
});
export default counterSlice.reducer;
export const { addNumber, minusNumber } = counterSlice.actions;
App.js
import "./App.css";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addNumber, minusNumber } from "./redux/modules/counterSlice";
function App() {
const globalNumber = useSelector((state) => state.counter.number);
const [number, setNumber] = useState(0);
const dispatch = useDispatch();
const onClickAddNumberHandler = () => {
dispatch(addNumber(+number));
};
const onClickMinusNumberHandler = () => {
dispatch(minusNumber(+number));
};
return (
<div>
<div>{globalNumber}</div>
<input
type="number"
onChange={(event) => setNumber(event.target.value)}
/>
<button onClick={onClickAddNumberHandler}>๋ํ๊ธฐ</button>
<button onClick={onClickMinusNumberHandler}>๋นผ๊ธฐ</button>
</div>
);
}
export default App;
2) Thunk ํจ์ ๋ง๋ค๊ธฐ
3) extraReducer์ thunk ๋ฑ๋กํ๊ธฐ
4) dispatch(thunk ํจ์) ํ๊ธฐ
counterSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
// createAsyncThunk API ํ์ฉํ๊ธฐ
// thunk๋ฅผ ์ฐ๋ ์ด์ ? ๋น๋๊ธฐ ํจ์๋ฅผ ์ํํ๊ธฐ ์ํด์ ์ฆ, ์๋ฒ์ ์์ฒญ์ ํ๊ธฐ ์ํด์
// thunk ํจ์๋ ์์ "__"๋ฅผ ๋ถ์ฌ์ค๋ค
// thunk ํจ์๋ ๋ ๊ฐ์ ์ธํ(์ด๋ฆ๊ณผ ํจ์)์ด ๋ค์ด๊ฐ๋ค
export const __addNumber = createAsyncThunk(
"ADD_NUMBER_WAIT",
// ์ด ํจ์์๋ ๋ ๊ฐ์ ์ธํ์ด ๋ค์ด๊ฐ๋ค
// payload์ thunk์ ๋ด์ฅ ๊ธฐ๋ฅ์ ๊ฐ์ง๊ณ ์๋ ๊ฐ์ฒด(thunkAPI)
// (2) payload์๋ฆฌ๋ก _addNumber์ ๋ฃ์ด์ค ์ซ์๊ฐ ๋ค์ด์ด
(payload, thunkAPI) => {
// ์ํํ๊ณ ์ ํ๋ ๋์ : 3์ด๋ฅผ ๊ธฐ๋ค๋ฆฌ๊ฒ ํ ์์
setTimeout(() => {
// (3) 3์ด ํ์ action creator๊ฐ ๋ฐ๋์ด ๋๋ฉด์ action ๊ฐ์ฒด๋ก ๋ฐ๋๊ณ dispatch๊ฐ ํธ์ถ๋จ
thunkAPI.dispatch(addNumber(payload));
}, 3000);
}
);
export const __minusNumber = createAsyncThunk(
"MINUS_NUMBER_WAIT",
(payload, thunkAPI) => {
setTimeout(() => {
thunkAPI.dispatch(minusNumber(payload));
}, 3000);
}
);
const initialState = {
number: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
addNumber: (state, action) => {
// (4) action.payload์ ์
๋ ฅํ ์ซ์๊ฐ ๋ค์ด์ค๊ณ state๊ฐ ์
๋ฐ์ดํธ ๋จ!
state.number = state.number + action.payload;
},
minusNumber: (state, action) => {
state.number = state.number - action.payload;
},
},
});
export default counterSlice.reducer;
export const { addNumber, minusNumber } = counterSlice.actions;
App.js
import "./App.css";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
// thunk ํจ์ import ํ๊ธฐ
import { __addNumber } from "./redux/modules/counterSlice";
import { __minusNumber } from "./redux/modules/counterSlice";
function App() {
const globalNumber = useSelector((state) => state.counter.number);
const [number, setNumber] = useState(0);
const dispatch = useDispatch();
const onClickAddNumberHandler = () => {
// dispatch(addNumber(+number));
// __addNumber(thunk ํจ์)๋ก ๋ฐ๊ฟ์ฃผ๊ธฐ
// (1) dispatch(__addNumber(+number))๋ฅผ ํตํด ์
๋ ฅํ number๊ฐ ๋ค์ด์ด
dispatch(__addNumber(+number));
};
const onClickMinusNumberHandler = () => {
// dispatch(minusNumber(+number));
dispatch(__minusNumber(+number));
// return๋ฌธ ์๋ต
๐ extraReducers
1) thunk ํจ์ ๊ตฌํ
2) ๋ฆฌ๋์ ๋ก์ง ๊ตฌํ : reducers -> extraReducers
- ์๋ฒ ํต์ : 100% ์ฑ๊ณต(x)
- ์ง๊ธ๊น์ง์ redux state(todos, counter)
- ์์ผ๋ก์ state(isLoading, isError, data)
3) ๊ธฐ๋ฅ ํ์ธ(network) - devTools
4) Store์ ๊ฐ์ ์กฐํ + ํ๋ฉด์ ๋๋๋ง
todosSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const initialState = {
todos: [],
// ์๋ฒ ํต์ ์ ์ํ state๋ฅผ ์ถ๊ฐ
isLoading: false,
isError: false,
error: null,
};
// (1-1) ์๋ฒ์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ (thunk ํจ์ ๊ตฌํ)
export const __getTodos = createAsyncThunk(
"getTodos",
// ์๋ฒ์ ํต์ ํ๋ ํจ์์ด๊ธฐ ๋๋ฌธ์ ๋ฐ๋์ ๋น๋๊ธฐ ํจ์์ฌ์ผ ํจ
async (payload, thunkAPI) => {
// ์๋ฒ์์ ํต์ ์ ํญ์ ์ฑ๊ณตํ ์ ์๋ ๊ฒ์ด ์๋๊ธฐ ๋๋ฌธ์ try catch๋ฌธ์ผ๋ก ์ค๋ฅ ์ ์ด
try {
const response = await axios.get("http://localhost:4000/todos");
console.log("response", response.data);
// (1-2) ์๋ฒ์์ ๊ฐ์ ธ์จ ๋ฐ์ดํฐ๋ฅผ ์ ํ๋ฆฌ์ผ์ด์
๋ด๋ถ์ ๋ฆฌ๋์ค ์คํ ์ด๋ก ๊ฐ์ ธ์ค๊ธฐ
// [fulfillWithValue] toolkit์์ ์ ๊ณตํ๋ API๋ก
// Promise ๊ฐ์ฒด๊ฐ resolve๋ ๊ฒฝ์ฐ(์ฆ, ๋คํธ์ํฌ ์์ฒญ์ด ์ฑ๊ณตํ ๊ฒฝ์ฐ) dispatch ํด์ฃผ๋ ๊ธฐ๋ฅ์ ๊ฐ์ง API
// dispatch ํด์ฃผ๋ ๊ธฐ๋ฅ? ์ด ๊ธฐ๋ฅ์ด ๋๋๊ณ ๋์ ๋ฆฌ๋์๋ก ๋ณด๋ด์ฃผ๋ ๊ธฐ๋ฅ
// dispatch? ๋ฆฌ๋์์๊ฒ ์ก์
๊ณผ ํ์ด๋ก๋๋ฅผ ์ ๋ฌํด์ state ์
๋ฐ์ดํธ ์ํค๋ ๊ณผ์
// response ์ค์์ ์๋ฏธ ์๋ ๋ถ๋ถ์ธ data๋ง ๋๊ฒจ์ค
return thunkAPI.fulfillWithValue(response.data);
} catch (error) {
console.log("error", error);
// [rejectWithValue] toolkit์์ ์ ๊ณตํ๋ API๋ก
// Promise ๊ฐ์ฒด๊ฐ reject๋ ๊ฒฝ์ฐ(์ฆ, ๋คํธ์ํฌ ์์ฒญ์ด ์คํจํ ๊ฒฝ์ฐ) dispatch ํด์ฃผ๋ ๊ธฐ๋ฅ์ ๊ฐ์ง API
return thunkAPI.rejectWithValue(error);
}
}
);
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
// (2) extraReducers๋ก ๋ฆฌ๋์ ๊ตฌํํ๊ธฐ
extraReducers: {
// ํต์ ์ด ์งํ์ค์ผ ๋
[__getTodos.pending]: (state, action) => {
// ์๋ฒ ํต์ ์ ์ํ state ์
๋ฐ์ดํธ
state.isLoading = true;
state.isError = false;
},
// ํต์ ์ด ์๋ฃ๋์ ๋
[__getTodos.fulfilled]: (state, action) => {
// ์๋ฒ ํต์ ์ ์ํ state ์
๋ฐ์ดํธ
state.isLoading = false;
state.isError = false;
// ์๋ฒ์์ ๋ฐ์์จ ๋ฐ์ดํฐ๋ฅผ state ์ค todos์ ๋ฃ์ด์ค
state.todos = action.payload;
// console.log("fulfilled: ", action);
// {type: 'getTodos/fulfilled', payload: undefined, ...}
},
// ํต์ ์ด ์คํจํ์ ๋
[__getTodos.rejected]: (state, action) => {
// ์๋ฒ ํต์ ์ ์ํ state ์
๋ฐ์ดํธ
state.isLoading = false;
state.isError = true;
// ์๋ฒ ํต์ ์ค ์ค๋ฅ๊ฐ ๋ฌ์ ๋ ๋ฐ์์จ error ๊ฐ์ฒด๋ฅผ error state์ ๋ฃ์ด์ค
state.error = action.payload;
},
},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;
App.js
import React from "react";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { __getTodos } from "./redux/modules/todosSlice";
const App = () => {
// (1-3) ๋ง์ดํธ๋ ๋ '__getTodos' ๊ฐ์ ธ์ค๊ธฐ
// thunk๋ฅผ ํธ์ถํ ๋๋ dispatch ํ์ํจ
const dispatch = useDispatch();
useEffect(() => {
// payload๋ ํ์์์!
dispatch(__getTodos());
}, []);
// (2-2) todos state์ ์๋ ๊ฒ๋ค์ ๊ตฌ์กฐ๋ถํดํ ๋น์ผ๋ก ๊ฐ์ ธ์ค๊ธฐ
// ๋ก๋ฉ์ค์ด๋ผ๋ฉด ๋ฐ๋ก ๋ก๋ฉ์ค์ ๋ฆฌํด, ๋ก๋ฉ ์๋ฃ ํ ์๋ฌ๊ฐ ๋๋ฉด ๋ฐ๋ก ์๋ฌ๋ฉ์ธ์ง๋ฅผ ๋ฆฌํดํจ์ผ๋ก์จ
// ๋ฐ์ผ๋ก ๋ด๋ ค๊ฐ ์ ์๋๋ก ํจ -> undefined๋ null๊ณผ ๊ด๋ จ๋ ์ค๋ฅ๊ฐ ๋์ง ์๋๋ก ์กฐ์น
const { isLoding, error, todos } = useSelector((state) => {
return state.todos;
});
// ๋ก๋ฉ์ค
if (isLoding) {
return <div>๋ก๋ฉ์ค...</div>;
}
// ์๋ฌ
if (error) {
return <div>{error.message}</div>;
}
// ์ ์
return (
<div>
{todos.map((todo) => {
return <div key={todo.id}>{todo.title}</div>;
})}
</div>
);
};
export default App;