# thunk에서 Promise 다루기
(1) Todos 조회하기 기능 구현
1. 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;
3. configStore 코드 추가
// 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) 구현 순서
예제 코드는 아래 순서에 따라 진행한다.
- thunk 함수 구현 → __ getTodos()
- 리듀서 로직 구현
- extraReducers 사용: reducers에서 바로구현되지 않는 기타 Reducer로직을 구현할 때 사용하는 기능이다. 보통 thunk 함수를 사용할 때 extraReducers를 사용한다.
- [중요 🔥] 통신 진행중, 실패, 성공에 대한 케이스를 모두 상태로 관리하는 로직을 구현한다. 서버와의 통신은 100% 성공하는 것이 아니다. 서버와 통신을 실패했을때도 우리의 서비스가 어떻게 동작할지 우리는 구현해야 한다. 또한 서버와의 통신은 ‘과정' 이다. 그래서 서버와 통신을 진행하고 있는 ‘진행중' 상태일때 우리의 서비스가 어떻게 작동해야할지 마찬가지로 구현한다.
- 기능확인
- devtools 이용해서 작동 확인
- Store 값 조회하고, 화면에 렌더링 하기
# 구현하기
(1) Thunk 함수 구현 → 서버에서 데이터 가져오기
먼저, initialState 에 대해서 설명하겠다.
isLoading은 서버에서 todos를 가져오는 상태를 나타내는 값 입니다. 초기값은 false이고, 서버와 통신이 시작되면 true였다가 통신이 끝나면 다시 false로 변경된다.
error는 만약 서버와의 통신이 실패한 경우 서버에서 어떤 에러 메시지를 보내줄텐데, 그것을 담아놓는 값입니다. 초기에는 에러가 없기때문에 null로 지정했다.
대부분 서버와의 통신을 상태관리 할때는 data, isLoading, error 로 관리한다.
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로부터 데이터를 잘 가져온 것을 볼 수 있다.
이제 서버에서 데이터를 가져오는 부분은 문제가 없으니, 가져온 데이터를 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]) 라고 작성하면 [__getTodos.fulfilled] 이 부분으로 디스패치가 된다.
그래서 action을 콘솔에 찍어보면 fulfillWithValue([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에 임시 데이터가 없으니 구분하기가 힘들기 때문에 { "id": 1, "title": "hello world!" } 라는 테스트 Todo를 하나 추가하고 진행하겠다.
아래와 같이 extraReducers 에 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;
(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를 화면에 보여줍니다.
// 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;
'코딩 > React' 카테고리의 다른 글
React Query (0) | 2023.04.29 |
---|---|
Custom Hooks (0) | 2023.04.29 |
Thunk (1) | 2023.04.28 |
axios 심화 - instance와 interceptor (0) | 2023.04.28 |
비동기 통신 - axios, fetch (0) | 2023.04.28 |
댓글