Thunk2

개발자지망생·2023년 11월 30일
0

React

목록 보기
20/24

1. thunk에서 Promise 다루기

  • (1) Todos 조회하기 기능 구현

    이전 챕터에서 사용했던 프로젝트를 이용하거나 또는 새로운 프로젝트를 생성하여, Redux 설정을 모두 하시고 아래 예시코드를 작성하시면 됩니다.

저번 챕터에서는 thunk를 통해서 3초를 기다리고, 이후에 숫자를 더하는 기능을 구현했습니다. 아주 간단한 예시였는데요. 이번 챕터에서는 조금 더 실용적인 예시를 다뤄보겠습니다.

json-server를 띄우고 Thunk 함수를 통해서 API를 호출하고 서버로부터 가져온 값을 Store에 dispatch 하는 기능입니다. 우리가 배운 것을 한번에 모두 사용하는 모든 개념들이 총 집합되어있는 기능이라고 생각하시면 됩니다.

시작에 앞서 아래 작업을 먼저 진행해주세요.

  1. json-server 설치 및 서버 가동 (db.json)
{
  "todos": []
}
  1. Slice로 todos 모듈 추가 구현 (우리가 이전에 01 챕터에서 작성했던 todos 모듈 뼈대입니다. 이미 작성되어 있다면 넘어가시면 됩니다.)
// src/redux/modules/todosSlice.js

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

const initialState = {
  todos: [],
};

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
});

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

모듈을 추가했으니, configStore에서도 리듀서를 추가해줘야겠죠? 01. 챕터에서 했던 코드입니다. 이미 되어 있다면, 넘어가시면 됩니다.

// src/redux/config/configStore.js

import { configureStore } from "@reduxjs/toolkit";
/**
 * import 해온 것은 slice.reducer 입니다.
 */
import counter from "../modules/counterSlice";
import todos from "../modules/todosSlice";

/**
 * 모듈(Slice)이 여러개인 경우
 * 추가할때마다 reducer 안에 각 모듈의 slice.reducer를 추가해줘야 합니다.
 *
 * 아래 예시는 하나의 프로젝트 안에서 counter 기능과 todos 기능이 모두 있고,
 * 이것을 각각 모듈로 구현한 다음에 아래 코드로 2개의 모듈을 스토어에 연결해준 것 입니다.
 */
const store = configureStore({
  reducer: { counter: counter, todos: todos },
});

export default store;
  • (2) 구현 순서

예제 코드는 아래 순서에 따라 진행합니다.

  1. thunk 함수 구현 → __ getTodos()
  2. 리듀서 로직 구현
    a. extraReducers 사용: reducers에서 바로구현되지 않는 기타 Reducer로직을 구현할 때 사용하는 기능입니다. 보통 thunk 함수를 사용할 때 extraReducers를 사용합니다.
    b. [중요 🔥] 통신 진행중, 실패, 성공에 대한 케이스를 모두 상태로 관리하는 로직을 구현합니다. 서버와의 통신은 100% 성공하는 것이 아닙니다. 서버와 통신을 실패했을때도 우리의 서비스가 어떻게 동작할지 우리는 구현해야 합니다. 또한 서버와의 통신은 ‘과정' 입니다. 그래서 서버와 통신을 진행하고 있는 ‘진행중' 상태일때 우리의 서비스가 어떻게 작동해야할지 마찬가지로 구현해야 합니다.
    1. 기능확인
      1. devtools 이용해서 작동 확인
    2. Store 값 조회하고, 화면에 렌더링 하기

2. 구현하기

  • (1) Thunk 함수 구현 → 서버에서 데이터 가져오기

먼저, initialState 에 대해서 설명하겠습니다.

isLoading은 서버에서 todos를 가져오는 상태를 나타내는 값 입니다. 초기값은 false이고, 서버와 통신이 시작되면 true였다가 통신이 끝나면 (성공 또는 실패를 하겠죠?) 다시 false로 변경됩니다.

error는 만약 서버와의 통신이 실패한 경우 서버에서 어떤 에러 메시지를 보내줄텐데요. 그것을 담아놓는 값입니다. 초기에는 에러가 없기때문에 null로 지정했습니다.

대부분 서버와의 통신을 상태관리 할때는 data, isLoading, error 로 관리합니다.

// src/redux/modules/todosSlice.js

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

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

// 우리가 추가한 Thunk 함수
export const __getTodos = createAsyncThunk(
  "getTodos",
  (payload, thunkAPI) => {}
);

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {}, // 새롭게 사용할 extraReducers를 꺼내볼까요?
});

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

thunk 함수를 아래와 같이 작성합니다. const data는 Promise를 반환합니다.

다시 말해 axios.get() (함수)은 Promise를 반환합니다. 그래서 반환된 Promise의 fullfilled 또는 rejected된 것을 처리하기위해 async/await 을 추가했습니다.

그리고 이 요청이 성공하는 경우에 실행되는 부분과 실패했을 때 실행되어야 하는 부분을 나누기 위해 try..catch 구문을 사용했습니다.

// src/redux/modules/todosSlice.js

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

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

// 완성된 Thunk 함수
export const __getTodos = createAsyncThunk(
  "todos/getTodos",
  async (payload, thunkAPI) => {
    try {
      const data = await axios.get("http://localhost:3001/todos");
      console.log(data);
    } catch (error) {
      console.log(error);
    }
  }
);

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {},
});

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

1차적으로 Thunk 함수의 구현이 끝났습니다. 이렇게 구현한 함수가 잘 작동하는지 1차적으로 한번 확인해보겠습니다. App.jsx 에 임시적으로 아래와 같은 코드를 구현해보겠습니다.

useEffect를 통해 App.js가 mount 됐을 때 thunk 함수를 dispatch 하는 코드입니다.

// src/App.jsx

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;

App.js 에서 콘솔을 보면, json-server로부터 데이터를 잘 가져온 것을 볼 수 있습니다. 우리가 db에 넣어준 todo가 없으니 빈 배열로 표시되고 있습니다. 그리고 리덕스 devtools 에서도 dispatch된 action을 잘 보여주고 있네요.

이제 서버에서 데이터를 가져오는 부분은 문제가 없으니, 가져온 데이터를 Store로 넣는 로직을 구현해보겠습니다.

  • (2) Thunk 함수 구현 → 가져온 데이터 Store로 dispatch 하기 썽크 함수에 아래 코드를 추가합니다.

**fulfillWithValue 는 툴킷에서 제공하는 API 입니다.**

Promise에서 **resolve**된 경우, 다시 말해 네트워크 요청이 성공한 경우에 dispatch 해주는 기능을 가진 API 입니다. 그리고 인자로는 payload를 넣어줄 수 있습니다.

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

Promise가 reject 된 경우, 네트워크 요청이 실패한 경우 dispatch 해주는 기능을 가진 API 입니다. 마찬가지로 인자로 어떤 값을 넣을 수 있습니다. 필자는 catch 에서 잡아주는 error 객체를 넣었습니다.

// src/redux/modules/todosSlice.js

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:3001/todos");
      return thunkAPI.fulfillWithValue(data.data);
    } catch (error) {
      return thunkAPI.rejectWithValue(error);
    }
  }
);

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {},
});

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

근데 생각해보니 각각의 API가 dispatch 해준다고 하는데, 어디로 dispatch를 해주는걸까요? dispatch라는 것은 리듀서에게 action과 payload를 전달해주는 과정인데 우리는 아직 아무런 리듀서를 작성한적이 없거든요? 맞습니다. 지금부터 그 리듀서를 구현해보겠습니다.

  • (3) 리듀서 로직 구현 → extraRecuders

Slice 내부에 있는 extraRecuders에서 아래와 같이 코드를 구현합니다. extraRecuders 에서는 아래와 같이 pending, fulfilled, rejected에 대해 각각 어떻게 새로운 state를 반환할 것인지 구현할 수 있습니다.

우리가 thunk 함수에서 thunkAPI.fulfillWithValue([data.data](http://data.data)) 라고 작성하면 [__getTodos.fulfilled] 이 부분으로 디스패치가 됩니다. 그래서 action을 콘솔에 찍어보면 fulfillWithValue([data.data](http://data.data)) 가 보낸 액션객체를 볼 수 있습니다. typepayload가 있죠!

아직은 어렵죠? 한번에 이해하기 어려운 개념입니다. 계속 반복해서 봅시다.

정리하자면 원래는 우리가 action creator를 만들고,

리듀서에서 스위치문을 통해서 구현해줘야 하는 부분을 모두 자동으로 해주고 있는 모습을 보고 계신겁니다.

// src/redux/modules/todosSlice.js

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:3001/todos");
      return thunkAPI.fulfillWithValue(data.data);
    } catch (error) {
      return thunkAPI.rejectWithValue(error);
    }
  }
);

export const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
  extraReducers: {
    [__getTodos.fulfilled]: (state, action) => {
      console.log("fulfilled 상태", state, action); // Promise가 fullfilled일 때 dispatch
    },
  },
});

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

이제, 각각의 상태로 thunkAPIdispatch 해주는 것을 확인했으니, 실제로 리듀서 로직을 구현해보겠습니다. db에 임시 데이터가 없으니 구분하기가 힘들군요. { "id": 1, "title": "hello world!" } 라는 테스트 Todo를 하나 추가하고 진행하겠습니다.

아래와 같이 extraReducerspendingrejected 상태에 따른 리듀서 로직을 추가로 구현해줍니다.

// src/redux/modules/todosSlice.js

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:3001/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;
  • (4) 기능 확인 리덕스 devtools를 보면 우리가 만든 기능이 정상적으로 작동하고 있음을 알 수 있습니다. App.jsx가 mount됐을 때 Thunk 함수가 dispatch되었고, Axios에 의해서 네트워크 요청이 시작됐습니다. 그래서 todos의 isLoading이 true로 변경된 것을 알 수 있습니다.

네트워크 요청이 끝났고, 성공했습니다. 그래서 thunkAPI.fulfillWithValue(data.data); 에 의해서 생성된 todos/getTodos/fulfillled 라는 액션이 dispatch가 되었고, 그로 인해 리듀서에서 새로운 payload를 받아 todos를 업데이트 시켰습니다. 그리고 네트워크가 종료되었으니 isLoading상태도 false로 변경되었습니다.

rejected 가 된 것을 보고자 한다면, 의도적으로 실패하게 네트워크 요청을 하면 됩니다. 이상한 url로 네트워크 요청을 보내는 것이죠.

결과를 보면, 역시 정상적으로 작동했음을 알 수 있습니다.

  • (5) Store 값 조회하고, 화면에 렌더링 하기

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

서버에서 data를 가져오는 동안에는 우리의 서비스를 사용하는 유저에게 ‘로딩중' 임을 표시합니다. 그리고 만약에 네트워크가 실패해서 정보를 가져오지 못한 경우, 에러 메시지를 보여줍니다. 위 두가지가 모두 아닌 경우에는 서버에서 불러온 todos를 화면에 보여줍니다. App.jsx 에 아래 코드를 작성해보세요.

// 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();
  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 App;

출처 : 스파르타코딩 클럽 2023 강의자료

profile
프론트엔드개발자를 목표로 공부중입니다.

0개의 댓글