[TIL #35] Redux 미들웨어 thunk함수 구현하기

Bora.K | 권보라·2023년 12월 4일
1

TIL

목록 보기
35/51
post-thumbnail

오늘 한 일



학습 내용


thunk

1. Redux 미들웨어란?

액션 → 미들웨어 → 리듀서 → 스토어

미들웨어는 리덕스에서 dispatch하여 action리듀서로 전달되기 전 중간 단계에서 추가적인 작업을 가능하게 한다. 즉, 서버와의 통신을 위해서 사용하는데, 가장 많이 사용하는 리덕스 미들웨어가 Redux-thunk이다.

2. Redux-thunk

(1) thunk란?

dispatch(() ⇒ {})

Redux-thunk를 사용하면 dispatch할 때 객체가 아닌 함수를 dispatch할 수 있다.
이 함수에 우리가 action을 리듀서로 보내기 전 중간 단계에서 수행하고 싶은 작업을 넣는다.

(2) thunk 함수 만들기
Redux-Toolkit에서는 **createAsyncThunk** 라는 API를 제공한다.

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

  • 첫 번째 인자 : Action Value 이름

  • 두 번째 인자 : 함수(수행하고 싶은 작업 구현)

    • 함수에는 (payload, thunkAPI) ⇒ {}
  • thunk 함수는 명시적으로 __ 를 함수 이름에 붙인다.

export const __addNumber = createAsyncThunk(
  "ADD_NUMBER_WAIT",
  (payload, thunkAPI) => {
    setTimeout(() => {
      thunkAPI.dispatch(addNumber(payload));
    }, 3000);
  }
);

3. thunk 함수 구현

통신과 관련된 부분을 관리하는 state

(1) 서버에서 data.json에 있는 데이터 가져오기

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

// 1. 초기값을 통신과 관련된 state로
const initialState = {
  todos: [],
  isLoading: false,
  isError: false,
  error: null,
};

// 2. createAsyncThunk로 thunk 함수 구현
export const __getTodos = createAsyncThunk(
  "todos/getTodos",
  async (payload, thunkAPI) => {
    try {
      // thunkAPI.fulfillWithValue 또는 thunkAPI.rejectWithValue
      // 이것을 이용하여 extraReducers로 보내줌
      const response = await axios.get("http://localhost:3001/todos");
      console.log("response", response);
    } catch {
      console.log("error", error);
    }
  }
);

// 3. 기존 리듀서 로직에 extraReducers 추가
export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {},
});

export const {} = todosSlice.actions;
export default todosSlice.reducer;

1. initialState
서버와의 통신은 100% 성공하는 것이 아니기 때문에 통신 진행중, 성공, 실패에 대한 케이스를 모두 상태로 관리하는 로직으로 initialState를 구현한다.

  • isLoading : 서버에서 todos를 가져오는 상태
  • error : 서버와의 통신이 실패한 경우 서버에서 보내는 에러 메세지를 담아 놓는 값

2. createAsyncThunk
axios.get()은 Promise를 반환한다.
반환된 Promise의 fullfilled 또는 rejected된 것을 처리하기 위해 async/await를 추가한다.

  • try : 요청이 성공하는 경우 실행되는 부분
  • catch : 실패하는 경우 실행되는 부분

🔎 Promise란?
Promise는 비동기 작업의 완료 또는 실패와 그 결과값을 나타내는 객체이다.
Promis를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있다.

예를 들어서 피자 배달을 기다리고 있다고 생각해 보자.
피자 가게에 전화를 걸면 곧 피자를 배달하겠다고 약속을 한다.

  1. 피자가 준비되면 집까지 배달되기를 기대한다.
  2. 문제가 있는 경우(예: 피자 품절) 알려준다.

- 대기 Pending : 전화를 걸 때의 초기 상태로 피자가 준비되기를 기다림
- 이행 fulfilled : 피자가 성공적으로 배달됨
- 거부 Rejected : 문제가 있어서 약속이 거부됨

3. extraReducers
reducers에서 바로 구현되지 않는 기타 Reducer 로직을 구현할 때 사용한다.

(2) 구현한 함수가 잘 작동하는지 확인

  • useEffect를 통해 App.js가 mount됐을 때 thunk함수를 dispatch하는 코드 구현
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { __getTodos } from "./redux/modules/todosSlice";

const App = () => {
  const dispatch = useDispatch();

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

  return <div>App</div>;
};

export default App;

(3) 가져온 데이터를 storedispatch하기

toolkit에서 제공하는 API

  • thunkAPI.fulfillWithValue : 네트워크 요청이 성공한 경우 그 value로 dispatch 해준다.
  • thunkAPI.rejectWithValue : 네트워크 요청이 실패한 경우 그 value로 dispatch 해준다.
export const __getTodos = createAsyncThunk(
  "getTodos",
  async (payload, thunkAPI) => {
    try {
      // thunkAPI.fulfillWithValue 또는 thunkAPI.rejectWithValue
      // 이것을 이용하여 extraReducers로 보내줌
      const response = await axios.get("http://localhost:3001/todos");
      return thunkAPI.fulfillWithValue(response.data);
    } catch (error) {
      return thunkAPI.rejectWithValue(error);
    }
  }
);

3. 리듀서 로직 구현 → extraReducers

(1) extraReducers

Slice 내부에 있는 extraReducers에서 pending, fulfilled, rejected에 대해 각각 어떻게 새로운 state를 반환할 것인지를 구현한다.

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {
    [__getTodos.pending]: (state, action) => {
      state.isLoading = true;
			state.isError = false;
    },
    [__getTodos.fulfilled]: (state, action) => {
      state.isLoading = false;
			state.isError = false;
      state.todos = action.payload;  // store에 있는 todos에 response.data가 인수로 옴
    },
    [__getTodos.rejected]: (state, action) => {
      state.isLoading = false;
			state.isError = true;
      state.error = action.payload;  // error가 인수로 옴
    },
  },
});

4. Store 값 조회하고 화면에 렌더링하기

useSelector를 이용해서 store 값을 조회하고 화면에 렌더링 할 수 있다. 각각의 상태에 따라 화면이 다르게 표시되도록 하여 로딩중이거나 실패했을 경우에는 렌더링되지 않도록 한다.

// src/App.jsx

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

const App = () => {
  const dispatch = useDispatch();

// useSelector를 이용하여 구조분해할당으로 isLoding, error, todos를 불러온다.
  const { isLoading, error, todos } = useSelector((state) => state.todos);

// useEffect로 App.js가 mount 되었을 때에만 실행한다.
  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 App;

오늘의 회고


오늘 개인 프로젝트 과제를 제출했다. 과제 내용은 지난 번 만들었던 팬페이지를 redux-toolkit으로 리팩토링하고 json-server를 이용하여 데이터 가져오기, 튜터님이 만들어두신 API 서버를 이용하여 로그인, 로그아웃, 프로필 페이지 구현하기였다.

처음에는 금방 끝날 줄 알았다... 왜 그런 생각을 했을까? 데이터를 가져오고 수정하고 저장하는데 오류가 무수히 많이 발생했다. 서버와 데이터 통신이 잘 이루어지면 UI에서 state를 변경하는데 바로 적용이 되지않고, 잘 수정했다 싶으면 다른 코드를 건드리다가 이전에 정상적으로 작동하던 기능들도 에러가 났다. 정말 중간에 포기하고 싶은 마음이 컸다. 시간도 오래 걸렸고, 며칠을 밤 늦게까지 작업을 했다.

결국 필수 기능도 다 구현을 못했다. 자신이 작성한 letter만 수정, 삭제가 가능하도록 구현했어야 했는데, 시간이 부족해서 결국 구현을 못하고 제출했다. 오늘 해설강의를 들으면서 하나하나 다시 정리하고 이해했다. 해설강의를 들으면서 내가 했던 방식들의 문제들도 보이고, 구현 못했던 기능들을 추가로 구현해 봐야겠다고 생각했다.


내일 할 일


  • [팀 프로젝트] 아웃소싱 프로젝트 발제
  • [팀 프로젝트] 리액트 심화 강의 완강
profile
Frontend Engineers

0개의 댓글