[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
    });
  }
);
  • ๋„ค์ด๋ฒ„ ๋ธ”๋Ÿฌ๊ทธ ๊ณต์œ ํ•˜๊ธฐ
  • ๋„ค์ด๋ฒ„ ๋ฐด๋“œ์— ๊ณต์œ ํ•˜๊ธฐ
  • ํŽ˜์ด์Šค๋ถ ๊ณต์œ ํ•˜๊ธฐ
  • ์นด์นด์˜ค์Šคํ† ๋ฆฌ ๊ณต์œ ํ•˜๊ธฐ