230707 Thunk

๋‚˜์œค๋นˆยท2023๋…„ 7์›” 7์ผ
0

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
16/55

๐Ÿ“Œ 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๋ฌธ ์ƒ๋žต
  • ๋ฆฌ๋•์Šค ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•ก์…˜์ด ๋ฆฌ๋“€์„œ๋กœ ์ „๋‹ฌ๋˜๊ธฐ ์ „์— '์ค‘๊ฐ„์—' ์–ด๋–ค ์ž‘์—…์„ ๋” ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • Thunk๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ์ฒด๊ฐ€ ์•„๋‹Œ ํ•จ์ˆ˜๋ฅผ dispatch ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฆฌ๋•์Šค ํˆดํ‚ท์—์„œ Thunk ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•  ๋•Œ๋Š” createAsyncThunk๋ฅผ ์ด์šฉํ•œ๋‹ค.
  • createAsyncThunk()์˜ ์ฒซ๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” action value๊ฐ€ ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” ํ•จ์ˆ˜๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค.
  • ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ ๋“ค์–ด๊ฐ€๋Š” ํ•จ์ˆ˜์—์„œ๋Š” ๋˜ ๋‘ ๊ฐœ์˜ ์ธ์ž๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ฒซ๋ฒˆ์งธ ์ธ์ž๋Š” ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ณด๋‚ด๋Š” payload๊ณ , ๋‘๋ฒˆ์งธ ์ธ์ž๋Š” thunk์—์„œ ์ œ๊ณตํ•˜๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๊ธฐ๋Šฅ์ด๋‹ค.

๐Ÿ“Œ 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;
profile
ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋ฅผ ๊ฟˆ๊พธ๋Š”

0๊ฐœ์˜ ๋Œ“๊ธ€