[React JS] [์ฐธ์กฐ] Redux
1. Redux๋?
- Redux is a predictable state container for JavaScript
- JS ์ดํ๋ฆฌ์ผ์ด์ ์ ์ํ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
Props VS State
Props (Properties)
- ๊ตฌ์ฑ์์๊ฐ ์๋ก ํต์ ํ๋ ๋ฐฉ๋ฒ (A->B ์ปดํฌ๋ํธ๊ฐ ๋ฐ์ดํฐ ๊ตํ)
- ์์ ๊ตฌ์ฑ ์์์์ ์๋์ชฝ์ผ๋ก ํ๋ฆ
- ํด๋น ๊ฐ์ ๋ณ๊ฒฝํ๋ ค๋ฉด ์์ ๊ด์ ์์ Props ๋ณ๊ฒฝ๊ฐ๋ฅ, ๋ถ๋ชจ๋ ๋ด๋ถ ์ํ๋ฅผ ๋ณ๊ฒฝํด์ผํจ
= ์์ ์ปดํฌ๋ํธ์ props๋ฅผ ๋ณ๊ฒฝํ๋ ค๋ฉด ๋ถ๋ชจ ์ปดํฌ๋ํธ์ state๋ฅผ ๋ฐ๊ฟ์ผํ๋ค๋ ๋ง (a bit immutable)
<ChatMessages
messages={messages}
currentMember={member}
>
State
- ๋ถ๋ชจ -> ์๋ ๊ฐ ์๋ ํ๋์ ์ปดํฌ๋ํธ ์์์ ๋ฐ์ดํฐ ์ ๋ฌ
ex) ๊ฒ์ ์ฐฝ์ ๊ธ์ ์ ๋ ฅํ ๋ ๊ธ์ด ๋ณํ๋ ๊ฒ์ state ๋ณ๊ฒฝ
- state is mutable
- state์ด ๋ณํ๋ฉด re-render๋จ
state = {
message: ''.
attachFile: undefined,
openMenu: false,
};
Redux๋ State๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ
Redux Data Flow (strict undirectional data flow)
ex) 10$ deposit
-> state: 0
-> UI: +10
-> EventHandler -> Dispatch Action (+10$)
-> Store State + 10$
Action
- action์ ๊ฐ๋จํ JavaScript ๊ฐ์ฒด
- ์ํํ๋ ์์ ์ ์ ํ์ ์ง์ ํ๋ type ์์ฑ ํฌํจ (ex.withdraw/deposit)
- ์ ํ์ ์ผ๋ก redux ์ ์ฅ์์ ์ผ๋ถ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๋ ๋ฐ ์ฌ์ฉ๋๋ payload ์์ฑ์ ๊ฐ์ง ์๋ ์์
- reducer๋ก action์ ๋์ง
Reducer
- ์ ํ๋ฆฌ์ผ์ด์ ์ํ์ ๋ณ๊ฒฝ ์ฌํญ์ ๊ฒฐ์ ํ๊ณ ์ ๋ฐ์ดํธ๋ ์ํ๋ฅผ ๋ฐํํ๋ ํจ์
- parameter๋ก ๋์ด์ค๋ arguments(์ธ์)๋ก ์กฐ์น๋ฅผ ์ทจํ๊ณ store ๋ด๋ถ ์ํ๋ฅผ ์ ๋ฐ์ดํธ
- ์ด์ state์ action object๋ฅผ ๋ฐ์ ํ next state๋ฅผ return
(previousState, action) => nextState
* Reducer๋ pure function - reducer ๋ด๋ถ์์ ํ์ง๋ง์์ผ ํ ๊ฒ๋ค
- Mutate its aruments
- Perform side effects like API calls and routing transitions
- Call non-pure functions (e.g. Date.now(), or Math.random())
Redux Store
- ์ด๋ค์ ํ๋๋ก ๋ชจ์ผ๋ ๊ฐ์ฒด ์ ์ฅ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ฒด ์ํ ํธ๋ฆฌ๋ฅผ ๋ณด์
- ๋ด๋ถ ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ์ ์ผํ ๋ฐฉ๋ฒ์ ํด๋น ์ํ์ ๋ํ Action์ ์ ๋ฌํ๋ ๊ฒ
- Redux Store๋ ํด๋์ค๊ฐ ์๋ (๋ช๊ฐ์ง Methods๊ฐ ์๋ ๊ฐ์ฒด)
2. Middleware ์์ด Redux counter app ๋ง๋ค๊ธฐ
๋ฆฌ์กํธ ์ฑ ์ค์น
npx create-react-app ./ --template typescript |
redux ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
npm install redux --save |
Counter UI ๋ฐ ํจ์ ์์ฑ
App.tsx
Reducer ์์ฑ
const counter = (state = 0, action: { type: string }) => {
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
return state;
}
};
export default counter;
App.tsx
import React from "react";
import "./App.css";
type Props = {
value: number;
onIncrement: () => void;
onDecrement: () => void;
};
function App({ value, onIncrement, onDecrement }: Props) {
return (
<div className="App">
Clicked: {value} times
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
}
export default App;
index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { createStore } from "redux";
import counter from "./reducers";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
const store = createStore(counter);
const render = () => root.render(
<React.StrictMode>
<App
value={store.getState()}
onIncrement={() => store.dispatch({ type: "INCREMENT" })}
onDecrement={() => store.dispatch({ type: "DECREMENT" })}
/>
</React.StrictMode>
);
render();
store.subscribe(render);
3. Combine Reducers
ํ๋์ ํฐ reducer์์ ์ฌ๋ฌ๊ฐ์ง reducer ๋ฃ๋ ๊ฒ
reducers>index.tsx
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
todo.tsx
enum ActionType {
ADD_TODO = "ADD_TODO",
DELETE_TODO = "DELETE_TODO",
}
interface Action {
type: ActionType;
text: string;
}
const todos = (state = [], action: Action) => {
switch (action.type) {
case "ADD_TODO":
return [...state, action.text]
default:
return state;
}
};
export default todos;
index.tsx
const store = createStore(rootReducer);
store.dispatch({
type: "ADD_TODO",
text: "USE_REDUX",
});
console.log("store.getState", store.getState());
const render = () =>
root.render(
<React.StrictMode>
<App
value={store.getState()}
onIncrement={() => store.dispatch({ type: "INCREMENT" })}
onDecrement={() => store.dispatch({ type: "DECREMENT" })}
/>
</React.StrictMode>
);
render();
store.subscribe(render);
4. Provider
- <Provider>์ ๊ตฌ์ฑ์์๋ Redux Store ์ ์ฅ์์ ์์ธ์คํด์ผํ๋ ๋ชจ๋ ์ค์ฒฉ๊ตฌ์ฑ์์์์ Redux Store ์ ์ฅ์๋ฅผ ์ฌ์ฉํ ์ ์๊ฒํจ
- React Redux ์ฑ์ ๋ชจ๋ React ๊ตฌ์ฑ ์์๋ ์ ์ฅ์์ ์ฐ๊ฒฐํ ์ ์์ผ๋ฏ๋ก ๋๋ถ๋ถ์ ์์ฉ ํ๋ก๊ทธ๋จ์ ์ ์ฒด ์ฑ์ ๊ตฌ์ฑ ์์ ํธ๋ฆฌ๊ฐ ๋ด๋ถ์์๋ ์ต์์ ์์ค์์ <Provider>๋ฅผ ๋ ๋๋งํจ
- ๊ทธ๋ฐ ๋ค์ Hooks ๋ฐ ์ฐ๊ฒฐ API๋ React์ ์ปจํ ์คํธ ๋ฉ์ปค๋์ฆ์ ํตํด ์ ๊ณต๋ ์ ์ฅ์ ์ธ์คํด์ค์ ์์ธ์ค ๊ฐ๋ฅ
npm install react-redux --save |
const render = () =>
root.render(
<React.StrictMode>
<Provider store={store}>
<App
value={store.getState()}
onIncrement={() => store.dispatch({ type: "INCREMENT" })}
onDecrement={() => store.dispatch({ type: "DECREMENT" })}
/>
</Provider>
</React.StrictMode>
);
type Props = {
value: any;
onIncrement: () => void;
onDecrement: () => void;
};
function App({ value, onIncrement, onDecrement }: Props) {
const [todoValue, setTodoValue] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTodoValue(e.target.value);
};
const addTodo = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setTodoValue("");
};
return (
<div className="App">
{/* Clicked: {value} times */}
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
<form onSubmit={addTodo}>
<input type="text" value={todoValue} onChange={handleChange} />
<input type="submit" />
</form>
</div>
);
}
5. useSelector & useDispatch (Hooks)
- <Provider>๋ก ๋๋ฌ์ธ๊ณ ์์ฑ์ store ๋ฃ์๋๋ฐ, ๊ทธ ์(provider๋ก ๋๋ฌ์ผ)์์๋ ์ปดํฌ๋ํธ์์๋ store ์ ๊ทผ ๊ฐ๋ฅ
- useSelector๋ก store์ ๊ฐ ๊ฐ์ ธ์ค๊ณ useDispatch๋ก action ๋ณด๋
useSelector
const counter = useSelector((state) => state.counter)
function App({ value, onIncrement, onDecrement }: Props) {
const dispatch = useDispatch();
const counter = useSelector((state: RootState) => state.counter);
const todos: string[] = useSelector((state: RootState) => state.todos);
const [todoValue, setTodoValue] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTodoValue(e.target.value);
};
const addTodo = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch({type:"ADD_TODO", text: todoValue})
setTodoValue("");
};
return (
<div className="App">
Clicked: {counter} times
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
<ul>
{todos.map((todo, index) => <li key={index}>{todo}</li>)}
</ul>
<form onSubmit={addTodo}>
<input type="text" value={todoValue} onChange={handleChange} />
<input type="submit" />
</form>
</div>
);
}
typescript๋ผ state 'Object is of type 'unknow' ์๋ฌ
-> ํด๊ฒฐ : RootState ํ์ ์์ฑ
export type RootState = ReturnType<typeof rootReducer>;
6. Redux Middleware
- [Action์ dispatch์ ์ ๋ฌํ๊ณ Reducer์ ๋๋ฌํ๋ ์๊ฐ] ์ฌ์ด์ ์ฌ์ ์ ์ง์ ๋ ์์ ์ ์คํํ ์ ์๊ฒ ํ๋ ์ค๊ฐ์
- ๋ก๊น , ์ถฉ๋๋ณด๊ณ , ๋น๋๊ธฐ API์ ํต์ , ๋ผ์ฐํ ๋ฑ์ ์ํด Redux ๋ฏธ๋ค์จ์ด ์ฌ์ฉ
๋ฆฌ๋์ค ๋ก๊น ๋ฏธ๋ค์จ์ด ์์ฑํ๊ธฐ
- ๋ก๊น ๋ฏธ๋ค์จ์ด ํจ์ ์์ฑ
const loggerMiddleware = (store) => (next) => (action) => {
// code
}
const LoggerMiddleware = function(store) {
return function(next) {
return function(action) {
// code
}
}
}
๋ ๋ค ๊ฐ์ ํจ์์
const loggerMiddleware = (store: any) => (next: any) => (action: any) => {
console.log("store", store);
console.log("action", action);
next(action);
};
const middleware = applyMiddleware(loggerMiddleware);
const store = createStore(rootReducer, middleware);
7. Redux Thunk
- Redux๋ฅผ ์ฌ์ฉํ๋ ์ฑ์์ ๋น๋๊ธฐ ์์ ์ ํ ๋ ๋ง์ด ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
- Redux Middleware
Thunk
- ์ผ๋ถ ์ง์ฐ๋ ์์ ์ ์ํํ๋ ์ฝ๋ ์กฐ๊ฐ
let x = 1+2
let foo = () => 1+2
// foo is a thunk
๋น๋๊ธฐ ์์ ์ ํด์ผํ ๋?
- ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๋
Axios module ์ค์น
npm install axios --save |
post reducer ์์ฑ
enum ActionType {
FETCH_POSTS = "FETCH_POSTS",
DELETE_POSTS = "DELETE_POSTS",
}
interface Post {
userId: number;
id: number;
title: string;
}
interface Action {
type: ActionType;
payload: Post[];
}
const posts = (state = [], action: Action) => {
switch (action.type) {
case "FETCH_POSTS":
return [...state, ...action.payload]
default:
return state;
}
};
export default posts;
posts ๋ฐ์ดํฐ๋ฅผ ์ํ ์์ฒญ ๋ณด๋ด๊ธฐ
- ์๋ฌ? : ์๋ ๊ฐ์ฒด๋ฅผ dispatchํด์ผํ๋๋ฐ ํจ์๋ฅผ dispatch ํด์ -> redux-thunk์ค์น
- action์ด ์์๋ ๊ฐ์ฒด๋ฉด์ reducer๋ก ๋ฐ๋ก ๊ฐ
- ๋ง์ฝ ํจ์๋ผ๋ฉด dispatch์ getState function์ ๋ค์ call
-> ๊ธฐ๋ค๋ฆฐ ํ์ ๋ค์ action ๋ง๋ค์ด์ ๊ฐ์ฒด๋ก ๋ฐํ
npm install redux-thunk --save |
๋ง์ฐฌ๊ฐ์ง๋ก store์ ์ถ๊ฐํด์ค์ผํจ
App.tsx
useEffect(() => {
dispatch(fetchPosts());
}, [dispatch]);
const fetchPosts = (): any => {
return async function fetchPostsThunk(dispatch: any, getState: any) {
const response = await axios.get(
"http://jsonplaceholder.typicode.com/posts"
);
dispatch({ type: "FETCH_POSTS", payload: response.data });
};
};
action๋ค์ actions ํด๋๋ก ๋ถ๋ฆฌ
import axios from "axios";
// export const fetchPosts = (): any => {
// return async function fetchPostsThunk(dispatch: any, getState: any) {
// const response = await axios.get(
// "http://jsonplaceholder.typicode.com/posts"
// );
// dispatch({ type: "FETCH_POSTS", payload: response.data });
// };
// };
export const fetchPosts = (): any => async (dispatch: any, getState: any) => {
const response = await axios.get("http://jsonplaceholder.typicode.com/posts");
dispatch({ type: "FETCH_POSTS", payload: response.data });
};
8. Redux Toolkit
- redux ๋ก์ง์ ์์ฑํ๊ธฐ ์ํ ๊ณต์ ๊ถ์ฅ ์ ๊ทผ ๋ฐฉ์
- Redux ์ฝ์ด๋ฅผ ๋๋ฌ์ธ๊ณ ์์ผ๋ฉฐ Redux ์ฑ์ ๋น๋ํ๋๋ฐ ํ์์ ์ธ ํจํค์ง์ ๊ธฐ๋ฅ ํฌํจ
- ๋จ์ํ, ์ค์ ๋ฐฉ์ง, Redux ์ ํ๋ฆฌ์ผ์ด์ ๋ ์ฝ๊ฒ ์์ฑํ ์ ์๋๋ก ํจ
Redux Toolkit Page
npx create-react-app ./ template redux-typescript |
Store ์์ฑ (๋ผ์์)
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
React์ redux ์ ๊ณต (provider๋ฅผ ํตํด)
Redux State Slice ์์ฑ (reducer ๋ถ๋ถ)
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';
import { fetchCount } from './counterAPI';
export interface CounterState {
value: number;
status: 'idle' | 'loading' | 'failed';
}
const initialState: CounterState = {
value: 0,
status: 'idle',
};
// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched. Thunks are
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount: number) => {
const response = await fetchCount(amount);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const counterSlice = createSlice({
name: 'counter',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
// The `extraReducers` field lets the slice handle actions defined elsewhere,
// including actions generated by createAsyncThunk or in other slices.
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
})
.addCase(incrementAsync.rejected, (state) => {
state.status = 'failed';
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state: RootState) => state.counter.value;
// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on current state.
export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
export default counterSlice.reducer;
Reducer
- ์ ํ๋ฆฌ์ผ์ด์ ์ํ์ ๋ณ๊ฒฝ ์ฌํญ์ ๊ฒฐ์ ํ๊ณ ์ ๋ฐ์ดํธ๋ ์ํ๋ฅผ ๋ฐํํ๋ ํจ์
- ์ธ์๋ก ์กฐ์น๋ฅผ ์ทจํ๊ณ store ๋ด๋ถ์ ์ํ๋ฅผ ์ ๋ฐ์ดํธ
- a function describing how the application's state changes
* redux ํดํท ์ฌ์ฉํ๋ฉด reducer์์ mutating ๋ก์ง ์ฌ์ฉ๊ฐ๋ฅ
* immer ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ค์ ๋ก ์ํ๊ฐ ๋ณํ์ง ์์
* ์ด์(draft) ์ํ์ ๋ํ ๋ณ๊ฒฝ์ฌํญ์ ๊ฐ์งํ๊ณ ์ด๋ฌํ ๋ณ๊ฒฝ์ฌํญ์ ๊ธฐ๋ฐ์ผ๋ก ์์ ํ ์๋ก์ด ๋ถ๋ณ(immutable)์ํ๋ฅผ ์์ฑ
= state ๋ณ๊ฒฝํด์ ๋ถ๋ณ์ฑ์ ์์งํค๋ ๊ฒ ๊ฐ์ง๋ง immer๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ ์ด์ draft state ์์ฑํด์ ๋ณํ๋ฅผ ๊ทผ๊ฑฐ๋กํด์ ๋ถ๋ณ์ฑ์ ์ง์ผ์ค
Store์ slice reducer ์ถ๊ฐ
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
react ์ปดํฌ๋ํธ์์ redux state ๋ฐ action ์ฌ์ฉ
- useSelector : ์ ์ฅ์์์ ๋ฐ์ดํฐ ์ฝ์
- useDispatch : action ์ ๋ฌ
increment action dispatchํด์ reducer์์ val+1
actionpayload์ amount ๋ค์ด๊ฐ๊ณ ๊ทธ๋งํผ ์ฆ๊ฐ
dispatch์์ action๋ฃ์ด์ฃผ๋ฉด๋จ
9. Redux Toolkit
1) Store ์์ฑ
๊ธฐ์กด Redux : createStore Redux Toolkit : configureStore
2) Action ์์ฑ
๊ธฐ์กด Redux : ์ก์ ํ์ ์์์ ์ก์ ์์ฑ์ ํจ์๋ฅผ ๋ถ๋ฆฌ ์ ์ธ
const INCREMENT = 'counter/increment'
function increment(amount: number) {
return {
type: INCREMENT,
payload: amount
}
}
const action = increment(10)
- type์ ์์์ ํ ๋น- action create function ->ํจ์ ํธ์ถํ๋ฉด ๊ฐ์ฒด ๋ฐํ (๊ฐ์ฒด ์์ type - ์์๊ฐ ๋ค์ด๊ฐ, payload - ์ธ์ ๋ค์ด์ด)
Redux Toolkit :
import {createAction} from '@reduxjs/toolkit';
const increment = createAction<number>('counter/increment');
const action = increment(10);
- createAction์ผ๋ก action ์์ฑ
- ์๋์ผ๋ก ํ์ ๋ฃ์ด์ฃผ๋ฉด ์๋์ผ๋ก actioncreate ํจ์ ๋ฐํ๋๊ณ ์ธ์๋ก ํธ์ถํ๋ฉด ๋ฐํ๋จ --> ํ์ ๋ง ๋ฃ์ผ๋ฉด ์๋์ผ๋ก ํจ์ ์์ฑ
3) Reducer ์์ฑ
๊ธฐ์กด Redux : switch๋ฌธ์ผ๋ก ์ด๋ฃจ์ด์ง ๋ฆฌ๋์ ํจ์
Redux Toolkit : createReducer ํจ์๋ฅผ ์ด์ฉ (initial state, (builder ์ฝ๋ฐฑํจ์))
-> createReducer์์ Action์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ผ์ด์ค ๋ฆฌ๋์๋ฅผ ์ ์ํ๋ ๋ ๊ฐ์ง ๋ค๋ฅธ ํํ์ธ "๋น๋ ์ฝ๋ฐฑ - builder callback" ํ๊ธฐ๋ฒ๊ณผ "๋งต ๊ฐ์ฒด(map object)" ํ๊ธฐ๋ฒ ์ง์
-> ๋ ๋ค ๋์ผํ์ง๋ง ๋น๋ ์ฝ๋ฐฑ ์ ํธ (ํ์ ์คํฌ๋ฆฝํธ ํธํ์ฑ ์ํด)
Builder Callback
- addCase๋ก ์ฒ๋ฆฌ (์ก์ ํ์ ๊ณผ ์ ํํ ๋งตํ๋๋ ์ผ์ด์ค ๋ฆฌ๋์๋ฅผ ์ถ๊ฐํ์ฌ ์ก์ ์ฒ๋ฆฌ)
- addMatcher ( ํจํด์ ๋ง๋ ๊ฒ์ ์ฒ๋ฆฌ)
- addDefaultCase (ํด๋น๋์ง ์๋ ๋ถ๋ถ ์ฒ๋ฆฌ)
Map Object
- initialState : ๋ฆฌ๋์๋ฅผ ์ฒ์ ํธ์ถํ ๋ ์ฌ์ฉํด์ผํ๋ ์ด๊ธฐ ์ํ ๊ฐ
- actionsMap : ์ก์ ํ์ ์ด ์ผ์ด์ค ๋ฆฌ๋์์ ๋งตํ๋์ด์๋ ๊ฐ์ฒด
- actionMatchers : {matcher, reducer}ํ์ ๋ฐฐ์ด - ์ผ์น ์ฌ๋ถ ๊ด๊ณ์์ด ๋ชจ๋ ์ผ์นํ๋ ๋ฆฌ๋์๊ฐ ์์๋๋ก ์คํ
- defaulstCaseReducer : ์คํ๋์ง ์์ ๊ฒฝ์ฐ ์คํ๋๋ ๊ธฐ๋ณธ ์ผ์ด์ค ๋ฆฌ๋์
Prepare ์ฝ๋ฐฑํจ์
- ํด๋น ํจ์ ์ฌ์ฉํด์ Action Contents ์ปค์คํฐ๋ง์ด์ง
- ์ผ๋ฐ์ ์ผ๋ก ์ก์ ์์ฑ์ ํจ์๋ฅผ ํธ์ถํด์ ์ก์ ์ ์์ฑํ ๋ ๋จ์ผ์ธ์ ๋ฐ์์ action.payload๊ฐ ์์ฑ (์์์์ฒ๋ผ)
- ์ด ๋ payload์ ์ฌ์ฉ์ ์ ์ ๊ฐ์ ์ถ๊ฐํ๊ณ ์ถ๋ค๋ฉด ์๋์ ๊ฐ์ด prepare callback ํจ์๋ฅผ ์ด์ฉ
import { creaetAction, nanoid } from '@reduxjs/toolkit'
const addTodo = createAction('todos/add', function prepare(text) {
return {
payload: {
text,
id: nanoid(),
createAt: new Date().toISOString(),
},
}
})
console.log(addTodo('Write more docs'))
createSlice()
- createSlice ๋ด๋ถ์์๋ ์ง๊ธ๊น์ง ๋ฐฐ์ด createAction๊ณผ createReducer๋ฅผ ์ฌ์ฉ
- createSlice ํจ์๋ ๋ฆฌ๋์ ํจ์์ ๋์์ธ ์ด๊ธฐ์ํ(initialState)์ slice์ด๋ฆ ์ ๋ฐ์ ๋ฆฌ๋์์ ์ํ์ ํด๋นํ๋ ์ก์ ์์ฑ์์ ์ก์ ํ์ ์ ์๋์ผ๋ก ์์ฑํ๋ ํจ์
* counter/decrement๋ผ๋ action type ์์๊ฐ ์์ฑ
* ์ด์ ์์ํ๋ ์ก์ ํ์ ์ ๊ฐ์ง ์ก์ ์ด ๋์คํจ์น ๋๋ฉด์ ๋ฆฌ๋์ ์คํ !
const todoSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer: (state, action) => {
state.push(action.payload)
},
prepare: (text) => {
const id = nonoid()
return { payload: { id, text } }
},
},
},
})
extraReducers
- createSlice๊ฐ ์์ฑํ action type ์ธ์ ๋ค๋ฅธ action type์ ์๋ต ๊ฐ๋ฅ
- ์ธ๋ถ ์ก์ ์ ์ฐธ์กฐํ๊ธฐ ์ํ ๊ฒ (slice.actions์์ ์์ฑ๋ ์ก์ ๊ฐ์ง์ง ์์)
createAsyncThunk
- createAction์ ๋น๋๊ธฐ ๋ฒ์ (createAction + Async)
// createAction
function createAction(type, prepareAction);
// createAsyncThunk
function createAsyncThunk(type, payloadCreator, options);
- ๋น๋๊ธฐ ์์ฒญ์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๋ํ๋ด๋ ์ถ๊ฐ Redux action type ์์๋ฅผ ์์ฑํ๋๋ฐ ์ฌ์ฉ๋๋ ๋ฌธ์์ด
> type
- 'users/requestStatus' type ์ธ์๋ ๋ค์ action type์ ์์ฑ
- pending: 'users/requestStatus/pending'
- fulfilled: 'users/requestStatus/fulffiled'
- rejected: 'users/requestStatus/rejected'
> payloadCreator
- Promise๋ฅผ ๋ฐํํ๋ ์ฝ๋ฐฑ ํจ์
const fetchUserById = createAsyncThunk(
'user/fetchByIdStatus',
async(userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// ์ด๋ฐ์์ผ๋ก
const userSlice = createSlice({
reducers: {
}
extraReducers: (builder) => {
builder.addCase(fetchUserById.pending, (state, action) => {
})
}
})
- dispatch(fetchUserById(123))
ํ๋ฉด userId์ 123 ๋ค์ด๊ฐ
paylaodCreator(arg, thunkAPI)
- ์ผ๋ฐ์ ์ผ๋ก Redux thunk ํจ์์ ์ ๋ฌ๋๋ ๋ชจ๋ ๋งค๊ฐ๋ณ์์ ์ถ๊ฐ ์ต์ ์ ํฌํจํ๋ ๊ฐ์ฒด
ใด ํด๋น ๊ฐ์ฒด์ ๋ค์ด์๋ property
-->
- dispatch: Redux store dispatch ๋ฉ์๋
- getState: Redux store getState ๋ฉ์๋
- extra: ์ค์ ์ thunk middleware์ ์ ๊ณต๋๋ ์ถ๊ฐ์ธ์ (์ฌ์ฉ๊ฐ๋ฅํ ๊ฒฝ์ฐ)
- requestId: ์ด ์์ฒญ ์ํ์ค๋ฅผ ์๋ณํ๊ธฐ ์ํด ์๋์ผ๋ก ์์ฑ๋ ๊ณ ์ ๋ฌธ์์ด ID ๊ฐ
- signal : ์ฑ ๋ก์ง์ ๋ค๋ฅธ ๋ถ๋ถ์ด ์ด ์์ฒญ์ ์ทจ์๊ฐ ํ์ํ ๊ฒ์ผ๋ก ํ์ํ๋์ง ํ์ธํ๋๋ฐ ์ฌ์ฉํ ์ ์๋ AbortController.signal ๊ฐ์ฒด
- rejectWithValue(value, [meta]): rejectWithValue๋ ์ ์๋ ํ์ด๋ก๋ ๋ฐ ๋ฉํ์ ํจ๊ป ๊ฑฐ๋ถ๋ ์๋ต์ ๋ฐํํ๊ธฐ ์ํด ์์ ์์ฑ์์์ ๋ฐํ(๋๋ throw)ํ ์ ์๋ ์ ํธ๋ฆฌํฐ ํจ์ -> ์ด๋ค ๊ฐ์ ์ฃผ๋ ์ ๋ฌํ๊ณ ๊ฑฐ๋ถ๋ ์์ ์ ํ์ด๋ก๋์ ๋ฐํ, ๋ฉํ๋ ์ ๋ฌํ๋ฉด ๊ธฐ์กด์ rejectAction.meta์ ๋ณํฉ๋จ
- fulfillWithValue(value, meta) : fulfillWithValue๋ fulfiledAction.meta์ ์ถ๊ฐํ ์ ์๋ ๊ธฐ๋ฅ์ ๊ฐ์ง๊ณ ์๋ ๋์ ๊ฐ์ผ๋ก ์ดํํ๊ธฐ ์ํด ์์ ์์ฑ์์์ ๋ฐํํ ์ ์๋ ์ ํธ๋ฆฌํฐ ํจ์
--> payload์ ํด๋น ๊ฐ๋ค์ด ๋ค์ด๊ฐ
cancellation
1) thunk ์คํ ์ค ์ทจ์ํ๊ธฐ
import React, { useEffect } from "react";
import { useAppDispatch } from "../../app/hooks";
import { incrementAsync } from "./counterSlice";
const Test = () => {
const dispatch = useAppDispatch();
useEffect(() => {
const promise = dispatch(incrementAsync(10));
return () => {
promise.abort();
};
}, []);
return <div>Test</div>;
};
export default Test;
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount: number) => {
const response = await fetchCount(amount);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
- toggle ๋ฒํผ ๋๋ฌ์ test ์ปดํฌ๋ํธ๊ฐ ๋ ์ด์ ์ฌ์ฉ๋์ง ์๋ Unmount ๋ ๋ clean up ๋ถ๋ถ์ด ํธ์ถ๋๋ฏ๋ก promise.abort() ๋ฉ์๋๊ฐ ํธ์ถ๋์ด์ thunk๊ฐ ์คํ ์ค์ ์ค์ง๋จ
-> ์ด๋ ๊ฒ ๋๋ฉด "thunkName/rejected" action์ด dispatch
2) abort ์ด๋ฒคํธ ๋ฐ์ ์ request๋ ์ทจ์ํ๊ธฐ
- reponse ๊ธฐ์กด๋๋กํ๋ฉด ์ ์ด
- abort ๋ฐ์ํ์ ๋ request๋ ์ทจ์ํ๋ ๋ฒ
AbortController
export const fetchUserAsync = createAsyncThunk(
"counter/fetchUsers",
async (_, { signal }) => {
const controller = new AbortController();
signal.addEventListener("abort", () => {
controller.abort();
});
await axios.get("https://jsonplaceholder.typicode.com/users", {
signal: controller.signal
});
}
);
'๐จโ๐ป Web Development > React JS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[React JS] 5. ํ์๊ฐ์ /๋ก๊ทธ์ธ/๋ก๊ทธ์์ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ (0) | 2023.07.17 |
---|---|
[React JS] 4. ๋ฐฑ์๋ ๊ธฐ๋ณธ๊ตฌ์กฐ ์์ฑํ๊ธฐ (0) | 2023.07.15 |
[React JS] 3. ํ๋ก ํธ์๋ ๊ธฐ๋ณธ๊ตฌ์กฐ ์์ฑํ๊ธฐ (0) | 2023.06.18 |
[React JS] 2. React.js (0) | 2023.06.08 |
[React JS] 1. Node.js (0) | 2023.05.21 |
์ต๊ทผ๋๊ธ