2-1) Async Action with Redux
(redux에서 비동기 액션처리(우리가 알고 있는 방식으로 접근해보기))
2-2) 리덕스 미들웨어
(2-1을 발전시켜서 미들웨어를 적용)
2-3) redux-devtools
2-4) redux-thunk
2-5) redux-promise-middleware
redux에서 비동기 처리를 위해서 어떻게 action을 처리하는지 알아보겠습니다.
redux 학습 때 사용했던 app을 이어서 계속 사용하겠습니다.
github의 api를 이용하여 유저정보를 가져오는 것을 해보겠습니다.
redux의 combineReducers와
react-redux의 useSelector와
react-redux의 Provider 를 사용하겠습니다.
https://api.github.com/users
위 주소로 get방식으로 가져오면 배열을 가지고 올 수 있습니다.
이것을 가져오기 위해서 axios를 설치하고, 우리의 컴포넌트에 user를 관리할 수 있는 user에 대한 state를 작성하겠습니다.
먼저 reducer에 users를 추가하기 위해서는 크게 3가지를 해주어야 합니다.
1. combineReducers에 users를 추가하고,
2. reducer에 user 처리로직 작성.
3. action 정의.
// reducers/reducer.js
import { combineReducers } from "redux";
import todos from "./todos";
import filter from "./filter";
import users from "./users";
const reducer = combineReducers({
todos: todos,
filter: filter,
users, // users를 추가합니다.
});
export default reducer;
// reducers/users.js
import { GET_USERS_FAIL, GET_USERS_START, GET_USERS_SUCCESS } from "../actions";
const initialState = {
loading: false,
data: [],
error: null,
};
export default function users(state = initialState, action) {
if(action.type === GET_USERS_START) {
return {
...state,
loading: true,
error: null,
};
}
if(action.type === GET_USERS_SUCCESS) {
return {
...state,
loading: false,
data: action.data,
};
}
if(action.type === GET_USERS_FAIL) {
return {
...state,
loading: false,
error: action.error,
};
}
return state;
}
user에 대한 reducer를 정의했습니다.
// actions.js
// users에 대한 액션 정의
// github api 호출을 시작하는 것을 의미합니다.
export const GET_USERS_START = "GET_USERS_START"; // 로딩시작
// github api 호출에 대한 응답이 성공적으로 돌아온 경우.
export const GET_USERS_SUCCESS = "GET_USERS_SUCCESS"; // 로딩 끝내고 데이터를 세팅합니다.
// github api 호출에 대한 응답이 실패한 경우.
export const GET_USERS_FAIL = "GET_USERS_FAIL"; // 로딩 끝내고 에러를 세팅합니다.
export function getUsersStart() {
return {
type: GET_USERS_START,
};
}
export function getUsersSuccess(data) {
return {
type: GET_USERS_SUCCESS,
data,
};
}
export function getUsersFail(error) {
return {
type: GET_USERS_FAIL,
error,
};
}
user에 대한 액션을 정의했습니다.
이제 유저목록을 보여줄 수 있는 컴포넌트와 컨테이너를 만들겠습니다. (아래)
// UserList.jsx
export default function UserList({ users }) {
if(users.length === 0) {
return <p>현재 유저 정보 없음</p>;
}
return (
<ul>
{users.map((user) => (
<li>{JSON.stringify(user)}</li>
))}
</ul>
);
}
// userListContainer.jsx
import UserList from "../components/UserList";
import { useSelector } from "react-redux";
export default function UserListContainer() {
const users = useSelector((state) => state.users.data);
return <UserList users={users} />;
}
만든 컴포넌트를 App.js에 추가합니다.
import "./App.css";
import TodoListContainer from "./containers/TodoListContainer";
import TodoFormContainer from "./containers/TodoFormContainer";
import UserListContainer from "./containers/UserListContainer";
function App() {
return (
<div className="App">
<header className="App-header">
<UserListContainer />
<TodoListContainer />
<TodoFormContainer />
</header>
</div>
);
}
이제 API를 호출해보겠습니다.
api 호출을 위해 라이브러리를 설치합니다.
npm i axios
API 호출은 보통 componentDidMount 시점에 해야합니다.
UserList.jsx
// components/UserList.jsx useEffect() 부분
// ...
useEffect(() => {
// await를 사용하기 위해 async가 필요합니다. async를 사용하기 위해 함수를 하나 만듭니다.
async function getUsers() {
try {
// 1. 시작한다.
const res = await axios.get('https://api.github.com/users');
// 2. 성공했다.
} catch(error) {
// 3. 실패했다.
}
}
}, []);
// ...
위까지 작성해 놓고 보면, 이제 redux의 dispatch로 시작한다 성공했다 실패했다 를 전달할 준비가 되었습니다.
시작한다 성공했다 실패했다를 함수를 만들어서 받아와야합니다.
// components/UserList.jsx
import { useEffect } from "react";
import axios from "axios";
export default function UserList({ users, start, success, fail }) {
useEffect(() => {
// await를 사용하기 위해 async가 필요합니다. async를 사용하기 위해 함수를 하나 만듭니다.
async function getUsers() {
try {
start();
const res = await axios.get("https://api.github.com/users");
success(res.data); // response를 성공적으로 받았으면..
} catch (error) {
fail(error);
}
}
getUsers(); // 만들어진 getUsers함수를 실행합니다.
}, [start, success, fail]); // 이 3개의 디펜던시는 이제 쉽게 바뀌지 않도록 잘 처리해야 합니다.
if (users.length === 0) {
return <p>현재 유저 정보 없음</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.login}</li>
))}
</ul>
);
}
// UserListContainer.jsx
import UserList from "../components/UserList";
import { useSelector, useDispatch } from "react-redux";
import { useCallback } from "react";
import { getUsersStart, getUsersSuccess, getUsersFail } from "../redux/actions";
export default function UserListContainer() {
const users = useSelector((state) => state.users.data);
const dispatch = useDispatch();
const start = useCallback(() => {
// dispatch를 받아와서 수행해야 합니다.
dispatch(getUsersStart());
}, [dispatch]);
const success = useCallback(
(data) => {
// dispatch를 받아와서 수행해야 합니다.
dispatch(getUsersSuccess(data));
},
[dispatch]
);
const fail = useCallback(
(error) => {
// dispatch를 받아와서 수행해야 합니다.
dispatch(getUsersFail(error));
},
[dispatch]
);
return <UserList users={users} start={start} success={success} fail={fail} />;
}
여기까지 제일 기본적인 방식으로 비동기 작업을 했습니다.
비동기 작업을 어디서 하느냐? 가 제일 중요합니다.
우리는 프리젠테이션 컴포넌트
에서 했습니다.
그래서 프리젠테이션 컴포넌트
가 많이 복잡해 졌습니다.
예를들어 액션을 분리해야 합니다. Start, Success, Fail ...등등
그리고 이 각각의 액션을 dispatch해주어야합니다. 왜냐하면 reducer는 동기적인 처리방식으로 작성을 해야하기 때문입니다.
Pure 함수여야하기 때문에 그 안에서 api를 호출하는 등의 로직을 넣을 수 없습니다.
이런식으로 작업을 하면 컴포넌트가 복잡해지고, 컴포넌트 안에 뷰와 상관없는 로직이 계속 끼어들게 됩니다.
여기서는 추가적인 라이브러리를 사용하지 않고, 이 비동기작업을 컴포넌트에서 컨테이너로 옮기도록 하겠습니다.
UserList.js 에서
async function getUsers() {
try {
start();
const res = await axios.get("https://api.github.com/users");
success(res.data); // response를 성공적으로 받았으면..
} catch (error) {
fail(error);
}
}
윗 부분이 비동기 작업입니다.
여기서 start와 success와 fail을 각각 만들어서 보냈지만, 이 로직들을 하나로 만들고, getUsers함수를 전달하는 방식으로 바꾸겠습니다.
// UserList.jsx
import { useEffect } from "react";
export default function UserList({ users, getUsers }) {
useEffect(() => {
getUsers();
}, [getUsers]);
if (users.length === 0) {
return <p>현재 유저 정보 없음</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.login}</li>
))}
</ul>
);
}
// containers/UserListContainer.jsx
import UserList from "../components/UserList";
import { useSelector, useDispatch } from "react-redux";
import { useCallback } from "react";
import { getUsersStart, getUsersSuccess, getUsersFail } from "../redux/actions";
import axios from "axios";
export default function UserListContainer() {
console.log("UserListContainer start");
const users = useSelector((state) => state.users.data);
const dispatch = useDispatch();
const getUsers = useCallback(async () => {
try {
dispatch(getUsersStart());
const res = await axios.get("https://api.github.com/users");
dispatch(getUsersSuccess(res.data));
} catch (error) {
dispatch(getUsersFail(error));
}
}, [dispatch]);
return <UserList users={users} getUsers={getUsers} />;
}
비동기 로직을 컴포넌트 안에서 하면 테스트하기도 어렵고, 뷰와 관련없는 로직이 추가되기 때문에 디버깅하기가 복잡할 수 있습니다. 그래서 이 컴포넌트는 props를 받아서 보여주는 용도로만 사용하고, 비동기 함수는 container에서 함수 한 벌로 만들면 깔끔한 코드를 만들 수 있고, 역할에 맞는 코드가 작성이 됩니다.
지금까지 middleware를 사용하지않고 redux에서 비동기처리를 하는 방법에 대해서 알아봤습니다.