Thunk 에서 Promise 다루기
Todos 조회하기 기능 구현
이전 챕터에서 사용했던 프로젝트를 이용하거나 또는 새로운 프로젝트를 생성해, Redux 설정을 모두 하고, 아래 코드를 작성했습니다.
저번 챕터에서는 thunk 를 통해서 3초를 기다리고, 이후에 숫자를 더하는 기능을 구현했습니다. 이번 챕터에서는 좀 더 실용적인 예시를 다뤄보겠습니다.
json-server 를 띄우고, Thunk 함수를 통해서 API 를 호출하고 서버로부터 가져온 값을 Store 에 dispatch 하는 기능입니다.
시작에 앞서 아래 작업부터 진행합니다.
1. json-server 설치 및 서버 가동
yarn add json-server
db.json
{
"todos": []
}
2. Slice 로 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 에서도 리듀서를 추가해줘야 합니다.
src/redux/config/configStore.js
import { configureStore } from "@reduxjs/toolkit";
/**
* import 해온 것은 slice.reducer 입니다.
*/
import counter from "../modules/counterSlice";
import todos from "../modules/todosSlice";
const store = configureStore({
reducer: { counter: counter, todos: todos },
});
export default store;
모듈(Slice)이 여러개인 경우, 추가할때마다 reducer 안에 각 모듈의 slice.reducer를 추가해줘야 합니다.
위 코드는 하나의 프로젝트 안에서 counter 기능과 todos 기능이 모두 있고, 이것을 각각 모듈로 구현한 다음에 아래 코드로 2개의 모듈을 스토어에 연결해준 것 입니다.
구현 순서
아래 순서에 따라 진행합니다.
- thunk 함수 구현 → __getTodos()
- 리듀서 로직 구현
- extraReducers 사용 : reducers 에서 바로 구현되지 않는 기타 Reducer 로직을 구현할 때 사용하는 기능입니다. 보통 thunk 함수를 사용할 때 extraReducers 를 사용합니다.
- 통신 진행 중, 실패 / 성공에 대한 케이스를 모두 상태로 관리하는 로직을 구현합니다. 서버와의 통신은 100% 성공하는 것이 아니기에 서버와 통신을 실패했을 대도 서비스가 어떻게 동작할지 구현해야 합니다. 또한 서버와의 통신은 '과정' 입니다. 그래서 서버와 통신을 진행하고 있는 '진행중' 상태일 때 서비스가 어떻게 작동해야할지 마찬가지로 구현해야 합니다.
- 기능 확인
- devtools 이용해 작동 확인
- Store 값 조회하고, 화면에 렌더링하기
Todos 조회하기 기능 구현하기
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 함수의 구현이 끝났습니다. 이렇게 구현한 함수가 잘 작동하는지 한 번 확인해보겠습니다. App.jsx 에 임시적으로 아래와 같이 코드를 입력했습니다.
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.jsx 에서 콘솔을 보면, json-server 로부터 데이터를 잘 가져온 것을 볼 수 있습니다. db에 넣어준 todo 가 없으니 빈 배열로 표시되고 있습니다. 그리고 리덕스 devtools 에서도 dispatch 된 action 을 잘 보여주고 있습니다.
Thunk 함수 구현 → 가져온 데이터 Store 로 dispatch 하기
이제 서버에서 데이터를 가져오는 부분은 문제가 없으니, 가져온 데이터를 Store 로 넣는 로직을 구현해보겠습니다.
Thunk 함수에 아래 코드를 추가했습니다.
fulfillWithVallue 는 툴킷에서 제공하는 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 라는 것은 리듀서에게 action 과 payload 를 전달해주는 과정인데 리듀서를 필요로 합니다. 때문에 아래에서 리듀서를 작성해보도록 하겠습니다.
리듀서 로직 구현 → extraReducers
Slice 내부에 있는 extraReducers 에서 아래와 같이 코드를 구현합니다. extraReducers 에서는 아래와 같이 pending, fulfilled, rejected 에 대해 각각 어떻게 새로운 state 를 반환할 것인지 구현할 수 있습니다.
thunk 함수에서 thunkAPI.fulfilledWithValue(data.data) 라고 작성하면 [__getTodos.fulfilled] 이 부분으로 디스패치가 이루어집니다. 그래서 action 을 콘솔에 찍어보면 fulfilledWithValue(data.data) 가 보낸 액션객체를 볼 수 있습니다. type 과 payload 가 있죠.
정리하자면, 원래는 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;
이제 각각의 상태로 thunkAPI 가 dispatch 해주는 것을 확인했으니, 실제로 리듀서 로직을 구현해보겠습니다. db에 임시 데이터를 테스트 Todo 를 하나 추가하고 진행하겠습니다.
{
"id": 1,
"title": "hello world!"
}
아래와 같이 extraReucers 에 pending 과 rejected 상태에 따른 리듀서 로직을 추가로 구현해줍니다.
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;
기능확인
리덕스 devtools 를 보면 기능이 정상적으로 작동하고 있음을 알 수 있습니다.
isLoading : true
App.jsx 가 mount 됐을 때 Thunk 함수가 dispatch 되었고, Axios 에 의해서 네트워크 요청이 시작됐습니다. 그래서 todos 의 isLoading 이 true 로 변경된 것을 알 수 있습니다.
thunkAPI.fulfilledWithValue(data.data);
네트워크 요청이 끝났고, 성공했습니다. 그래서 thunkAPI.fulfilledWithValue(data.data); 에 의해서 생성된 todos/getTodos/fulfilled 라는 액션이 dispatch 가 되었고, 그로 인해 리듀서에서 새로운 payload 를 받아 todos 를 업데이트 시켰습니다. 그리고 네트워크가 종료되었으니 isLoading 상태도 false 로 변경되었습니다.
Store 값 조회하고, 화면에 렌더링하기
이제 모든 로직을 구현했으니, useSelector 를 이용해서 store 값을 조회하고, 화면에 렌더링해보겠습니다. 이 부분은 기존과 동일합니다. 다만 각각의 상태에 따라 화면이 다르게 표시되어야 하는 부분이 추가되었습니다.
서버에서 data 를 가져오는 동안에는 서비스를 사용하는 유저에게 '로딩중' 임을 표시합니다. 그리고 만약에 네트워크가 실패해서 정보를 가져오지 못한 경우, 에러 메시지를 보여줍니다. 위 두가지가 모두 아닌 경우에느 서버에서 불러온 todos 를 화면에 보여줍니다.
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;
'항해 16기 > Today I Learned' 카테고리의 다른 글
[항해 34일차] TIL_React 심화주차: React Query (0) | 2023.09.12 |
---|---|
[항해 33일차] TIL_React 심화주차: Custom Hooks (0) | 2023.09.12 |
[항해 31일차] TIL_React 심화주차: Thunk (0) | 2023.09.11 |
[항해 30일차] TIL_React 심화주차: axios 심화 - instance & interceptor (0) | 2023.09.07 |
[항해 29일차] TIL_React 심화주차: 비동기 통신 - axios, fetch (0) | 2023.09.07 |