천진난만 코딩 스토리

2023.03.13) 항해 36일차 (1-미들웨어, thunk) 본문

TIL(Today I Learned)

2023.03.13) 항해 36일차 (1-미들웨어, thunk)

Wisdom_1104 2023. 3. 13. 20:50

지금 이 내용들은 "아 이렇게 쓰는거구나.", "이런식으로 사용하면 되겠네"라는 이해는 되지만

막상 과제에서는 잘 적용할 수 있을지 모르겠다.

사실 걱정되는 것이 이번 주차는 저번 주차의 과제를 하고 강의를 늦게 보기 시작해서 속도가 많이 느리다...

그래서 더 분발하고 있지만 내용이 어려운 만큼 시간이 부족한 것 또한 사실이다..

그래서 과제 두 개를 모두 해낼 수 있을지도 모르겠다.

그렇지만 일단 기죽지는 않기로 했다!!

 

1) Redux 미들웨어

① 미들웨어란?

리덕스에서 dispatch를 하면 action 이 리듀서로 전달이 되고, 리듀서는 새로운 state를 반환한다.

근데 미들웨어를 사용하면 이 과정 사이에 우리가 하고 싶은 작업들을 넣어서 할 수 있다.

 

만약 counter 프로그램에서 더하기 버튼을 클릭했을 때 바로 +1를 더하지 않고 3초를 기다렸다가,

+1이 되도록 구현하려면 미들웨어를 사용하지 않고서는 구현할 수 없다.

왜냐하면 dispatch가 되자마자 바로 action이 리듀서로 달려가서 새로운 state를 반환해버리기 때문이다.

즉, 여기서 “3초를 기다리는 작업" 이 작업을 미들웨어가 해주는 것이다.

 

보통 우리가 리덕스 미들웨어를 사용하는 이유는 서버와의 통신을 위해서 사용하는 것이 대부분이다.

 

 

2) thunk

① thunk란

리덕스에서 많이 사용하고 있는 미들웨어중에 하나이다.

thunk를 사용하면 우리가 dispatch를 할때 객체가 아닌 함수를 dispatch 할 수 있게 해준다.

즉 dispatch(객체) 가 아니라 dispatch(함수)를 할 수 있게 되는 것이다! 와우!!

 

그래서 중간에 우리가 하고자 하는 작업을 함수를 통해 넣을 수 있고, 그것이 중간에 실행이 되는 것이다.

그래서 아래 흐름과 같이 실행이 되는 것이고 , 이 함수를 thunk 함수라고 부른다.

dispatch(함수) → 함수실행 → 함수안에서 dispatch(객체)

 

 

② thunk 사용하기

툴킷에서는 createAsyncThunk 라는 API를 사용해서 thunk 함수를 생성할 수 있다. 

이 API는 함수인데, 첫번째 인자에는 Action Value, 두번째 인자에는 함수가 들어간다.

이 함수에 우리가 하고 싶은 작업들을 구현하면 된다.

두번째로 들어가는 함수에서 2개의 인자를 꺼내 사용할 수 있는데,

첫번째 인자는 컴포넌트에서 보내준 payload이고, 두번째 인자는 thunk에서 제공하는 여러가지 기능이다.

 

fulfillWithValue 는 툴킷에서 제공하는 API 이다.

Promise에서 resolve된 경우, 다시 말해 네트워크 요청이 성공한 경우에 dispatch 해주는 기능을 가진 API 이다.

그리고 인자로는 payload를 넣어줄 수 있다.

rejectWithValue 도 툴킷에서 제공하는 API 이다.

Promise가 reject 된 경우, 네트워크 요청이 실패한 경우 dispatch 해주는 기능을 가진 API 이다.

마찬가지로 인자로 어떤 값을 넣을 수 있다.

// thunk 함수는 createAsyncThunk 라는 툴킷 API를 사용해서 생성한다.

// __가 함수 이름에 붙는 이유는 이 함수가 thunk 함수라는 것을 표시하기 위한 개인의 convention이다.

export const __addNumber = createAsyncThunk(
	"ADD_NUMBER_WAIT",
	(arg, thunkAPI)=>{},
);

 

③ 코드로 알아보기 1

thunk 함수를 만들어보자!

thunk 함수의 역할은 “3초를 기다리는 것” 이다.

그리고 3초가 지나면 원래 하려고 했던 ADD_NUMBER를 해주는 것 까지가 thunk함수가 해야 할 일 이다.

// src/redux/modules/counterSlice.js

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";

export const __addNumber = createAsyncThunk(
	// 첫번째 인자 : action value
  "addNumber", 
	// 두번째 인자 : 콜백함수 
  (payload, thunkAPI) => {
    setTimeout(() => {
      thunkAPI.dispatch(addNumber(payload));
    }, 3000);
  }
);

const initialState = {
  number: 0,
};

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    addNumber: (state, action) => {
      state.number = state.number + action.payload;
    }
  },
});

export const { addNumber } = counterSlice.actions;
export default counterSlice.reducer;

모듈에서 thunk로 3초 기다린 후에 실행되도록 한다.

// src/App.jsx

import React from "react";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { __addNumber } from "./redux/modules/counterSlice";

const App = () => {
  const dispatch = useDispatch();
  const [number, setNumber] = useState(0);
  const globalNumber = useSelector((state) => state.counter.number);

  const onChangeHandler = (evnet) => {
    const { value } = evnet.target;
    setNumber(+value);
  };

  // thunk 함수를 디스패치한다. payload는 thunk함수에 넣어주면,
  // 리덕스 모듈에서 payload로 받을 수 있다.
  const onClickAddNumberHandler = () => {
    dispatch(__addNumber(number));
  };

  return (
    <div>
      <div>{globalNumber}</div>
      <input type="number" onChange={onChangeHandler} />
      <button onClick={onClickAddNumberHandler}>더하기</button>
    </div>
  );
};

export default App;

thunk로 만든 함수를 적용한다.

 

④ 코드로 알아보기 2

통신 진행중, 실패, 성공

//모듈
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";

const initialState = {
  todos: [],
  isLoading: false,
  error: null,
};

export const __getTodos = createAsyncThunk(
  "todos/getTodos",
  async (payload, thunkAPI) => {
    try {
      const data = await axios.get("http://localhost:4000/todos");
      return thunkAPI.fulfillWithValue(data.data);
    } catch (error) {
      return thunkAPI.rejectWithValue(error);
    }
  }
);

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {
    [__getTodos.pending]: (state) => {
      state.isLoading = true; // 네트워크 요청이 시작되면 로딩상태를 true로 변경합니다.
    },
    [__getTodos.fulfilled]: (state, action) => {
      state.isLoading = false; // 네트워크 요청이 끝났으니, false로 변경합니다.
      state.todos = action.payload; // Store에 있는 todos에 서버에서 가져온 todos를 넣습니다.
    },
    [__getTodos.rejected]: (state, action) => {
      state.isLoading = false; // 에러가 발생했지만, 네트워크 요청이 끝났으니, false로 변경합니다.
      state.error = action.payload; // catch 된 error 객체를 state.error에 넣습니다.
    },
  },
});

export const {} = todosSlice.actions;
export default todosSlice.reducer;
//브라우저
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { __getTodos } from "../redux/modules/todosSlice";

function Second() {
  const dispatch = useDispatch();
  const { isLoading, error, todos } = useSelector((state) => state.todos);

  useEffect(() => {
    dispatch(__getTodos());
  }, [dispatch]);

  if (isLoading) {
    return <div>로딩 중....</div>;
  }

  if (error) {
    return <div>{error.message}</div>;
  }

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>{todo.title}</div>
      ))}
    </div>
  );
}

export default Second;