1. thunk에서 Promise 다루기
json-server를 띄우고 Thunk 함수를 통해서 API를 호출하고 서버로부터 가져온 값을 Store에 dispatch 하는 기능입니다.
시작에 앞서 아래 작업을 먼저 진행해주세요.
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;
모듈을 추가했으니, 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) 구현 순서
1. thunk 함수 구현 → __ getTodos()
2. 리듀서 로직 구현
- extraReducers 사용: reducers에서 바로구현되지 않는 기타 Reducer로직을 구현할 때 사용하는 기능입니다. 보통 thunk 함수를 사용할 때 extraReducers를 사용합니다.
- 통신 진행중, 실패, 성공에 대한 케이스를 모두 상태로 관리하는 로직을 구현합니다. 서버와의 통신은 100% 성공하는 것이 아닙니다. 서버와 통신을 실패했을때도 서비스가 어떻게 동작할지 우리는 구현해야 합니다. 또한 서버와의 통신은 ‘과정' 입니다. 그래서 서버와 통신을 진행하고 있는 ‘진행중' 상태일때 서비스가 어떻게 작동해야할지 마찬가지로 구현해야 합니다.
3. 기능확인
- devtools 이용해서 작동 확인
4. 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 에 임시적으로 아래와 같은 코드를 구현해보겠습니다.
코드 수정: 파일이름 수정 todos → todosSlice
// 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가 없으니 빈 배열로 표시되고 있습니다.
이제 서버에서 데이터를 가져오는 부분은 문제가 없으니, 가져온 데이터를 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 - React Query (0) | 2023.12.09 |
---|---|
React - Custom Hooks (0) | 2023.12.09 |
React - Thunk(feat. Redux)란? (1) | 2023.11.30 |
React - axios(instance & interceptor) (1) | 2023.11.30 |
React - 비동기 통신(axios & fetch) (0) | 2023.11.29 |