[TIL] Redux Thunk

·2023년 11월 30일
1

TIL

목록 보기
46/85
post-thumbnail

Middleware 란?

업로드중..

미들웨어는 action이 dispatch 되어서 reducer에서 이를 처리하기 전에 사전에 지정된 작업들을 설정한다. 미들웨어를 action과 reducer 사이의 중간자 라고 이해하면 된다.

만약 counter 프로그램에서 + 버튼을 클릭했을 때 바로 +1을 하지 않고 3초를 기다렸다가 +1이 되도록 구현하려면 미들웨어를 사용하지 않고는 불가능하다.
왜냐하면 dispatch가 되지 마자 바로 action이 reducer로 달려가서 새로운 state를 반환해버리기 때문이다. 즉 여기서 "3초를 기다리는 작업" 을 미들웨어가 해 주는 것이다.

보통 우리가 리덕스 미들웨어를 사용하는 이유는 서버와의 통신을 위해 사용하는 것이 대부분이이고, 그 중에서도 많이 사용되고 있는 리덕스 미들웨어는 "redux-thunk" 라는 것이 있다.

Redux-thunk

(0) thunk 소개

thunk 란? : 특정 작업을 나중에 하도록 미루기 위해서 함수 형태로 감싼 것.
redux-thunk 란? : 리덕스에서 많이 사용하고 있는 미들웨어 중 하나.
이를 사용하면 우리가 dispatch를 할 때 객체가 아닌 함수를 dispatch 할 수 있게 해준다.

dispatch({ 액션객체 }) ❌
dispatch( function(){} ) ⭕️

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

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

[구현 과정]
1. thunk 함수 만들기 : createAsyncThunk (RTK 내장 api)
2. createSlice -> extraReducer에 thunk 등록하기
3. dispath(thunk함수) 하기
4. 기능 확인 (network + devTools)
5. store의 값을 조회 + 화면에 렌더링

(1) thunk 함수 만들기

  • 첫 번째 인자: action value
  • 두 번째 인자 : 콜백 함수
    • 콜백 함수 첫 번째 인자 : 컴포넌트에서 보낸 payload
    • 콜백 함수 두 번째 인자 : thunk에서 제공하는 여러가지 기능

counterSlice 예시

// thunk 함수는 createAsyncThunk 라는 툴킷 API를 사용해서 생성한다.
// __가 함수 이름에 붙는 이유는 이 함수가 thunk 함수라는 것을 표시하기 위한 개인의 convention 입니다.
export const __addNumber = createAsyncThunk(
  "ADD_NUMBER_WAIT",
  (payload, thunkAPI) => {
    setTimeout(() => {
      thunkAPI.dispatch(addNumber(payload)); // dispatch(액션객체)
    }, 3000);
  }
);

todosSlice 예시

export const __getTodos = createAsyncThunk(
  "GET_TODOS",
  async (payload, thunkAPI) => {
    try {
      const response = await axios.get("http://localhost:4000/todos");
      // Promise -> resolve(=네트워크 요청이 성공한 경우) dispatch 해 주는 기능을 가진 API
      return thunkAPI.fulfillWithValue(response.data); // action.payload
    } catch (error) {
      // Promise -> reject(=네트워크 요청이 실패한 경우) dispatch 해 주는 기능을 가진 API
      return thunkAPI.rejectWithValue(error); // action.payload
    }
  }
);
  • fulfillWithValue : RTK에서 제공하는 API
    • Promise에서 resolve 된 경우 (네트워크 요청이 성공한 경우) dispatch 해준다. 인자로는 payload를 넣었다.
  • rejectWithValue : RTK에서 제공하는 API
    • Promise가 reject 된 경우 (네트워크 요청이 실패한 경우) dispatch 해 준다. 인자는 catch에서 잡아준 error 객체를 넣었다.

👉 각각의 API가 dispatch를 해준다고 하는데, 어디로 dispatch를 해주는 것일까?
dispatch라는 것은 리듀서에게 action과 payload를 전달해주는 과정인데 우리는 아직 아무런 리듀서를 작성하지 않았다.

(2) extraReducer에 thunk 등록하기

const initialState = {
  todos: [
    {
      id: shortid.generate(),
      title: "제목",
      body: "내용",
      isDone: false,
    },
  ],
  isLoading: false,
  isError: false,
  error: null,
};

const todoSlice = createSlice({
  name: "todos",
  initialState,
  extraReducers: {
    [__getTodos.pending]: (state, action) => {
      // 아직 진행중 일 때
      state.isLoading = true; // 네트워크 요청이 시작되면 로딩상태를 true로 변경
      state.isError = false;
    },
    [__getTodos.fulfilled]: (state, action) => {
      // 요청 성공했을 때
      state.isLoading = false; // 네트워크 요청이 끝났으니 로딩상테를 false로 변경
      state.isError = false;
      state.todos = action.payload;  // store에 있는 todos에 서버에서 가져온 todos를 넣는다.
      console.log("fullfilled action", action); // {type: 'GET_TODOS/fulfilled', payload: undefined, ...}
    },
    [__getTodos.rejected]: (state, action) => {
      // 요청 실패했을 때
      state.isLoading = false; // (에러가 발생했지만) 네트워크 요청이 끝났으니 로딩상태를 false로 변경
      state.isError = true;
      state.error = action.payload; // catch된 error객체를 state.error에 넣는다.
    },
  },
});

createSlice 내부에 있는 extraRecuders에서 위와 같이 코드를 구현한다.
extraRecuders에서는 위처럼 pending, fulfilled, rejected에 대해 각각 어떻게 새로운 state를 반환할 것인지에 대해 구현할 수 있다.

예를 들어 우리가 thunk 함수에서

thunkAPI.fulfillWithValue(response.data);

라고 작성하면

[__getTodos.fulfilled]

이 부분으로 dispatch가 된다.
그래서 action을 콘솔에 찍어보면 fulfillWithValue(response.data)가 보낸 액션 객체를 확인할 수 있다. (type과 payload가 있는)

(3) dispath(thunk함수)

import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { __getTodos } from "../redux/modules/todos";

const Todos = () => {
  const dispatch = useDispatch();
  const { isLoading, error, todos } = useSelector((state) => state.todos);
  console.log("isLoading", isLoading, "error", error, "todos", todos);

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

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

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

export default Todos;

모든 로직을 구현했으니 useSelector를 이용하여 store의 값을 조회하고, 화면에 렌더링할 수 있다. 이 부분은 기존과 동일하다.
다만 각각의 상태에 따라 화면이 다르게 표시되어야 하는 부분이 추가되었다.

profile
느리더라도 조금씩, 꾸준히

0개의 댓글