01. API Fetching & Context API
데이터 패칭시 Reducer 와 Context를 함께 써보자
데이터 패칭 시 Reducer와 Context를 함께 쓰면 더 좋을 때가 있다.
외부 데이터들 같은 경우는 그 때 그 때 외부 API를 요청하면 되지만, 사이트 전역적으로 쓰일 데이터 ( 예를 들면 유저 데이터) 와 같은 것들은 Context와 함께 연동해서 쓰면 더욱 좋다.
Context 코드를 따로 분리하여 Provider, State, dispatch를 가져다 쓸 수 있는 함수와 Reducer함수, state등을 따로 분리한다.
- UsersContext.js
→ getUsers, getUser ( data fetching )
→ reducer, initial state, loading State, error state
→ provider, useUsersState(Hook), useUsersDispatch(Hook)
//================== 데이터 패칭 함수 ===================//
export const getUsers = async (dispatch) => {
dispatch({ type: "GET_USERS" });
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
dispatch({
type: "GET_USERS_SUCCESS",
data: response.data,
});
} catch (err) {
dispatch({
type: "GET_USERS_ERROR",
error: err,
});
}
};
export const getUser = async (dispatch, id) => {
dispatch({ type: 'GET_USER' });
try {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${id}`
);
dispatch({ type: 'GET_USER_SUCCESS', data: response.data });
} catch (e) {
dispatch({ type: 'GET_USER_ERROR', error: e });
}
}
//================== 상태값 세팅 ===================//
const initialState = {
users: {
loading: false,
data: null,
error: null
},
user: {
loading: false,
data: null,
error: null
}
};
// 로딩중일 때 바뀔 상태 객체
const loadingState = {
loading: true,
data: null,
error: null
};
// 성공했을 때의 상태 만들어주는 함수
const success = data => ({
loading: false,
data,
error: null
});
// 실패했을 때의 상태 만들어주는 함수
const error = error => ({
loading: false,
data: null,
error: error
});
//================== 리듀서 함수 ===================//
const usersReducer = (state, action) => {
switch (action.type) {
case 'GET_USERS':
return {
...state,
users: loadingState
};
case 'GET_USERS_SUCCESS':
return {
...state,
users: success(action.data)
};
case 'GET_USERS_ERROR':
return {
...state,
users: error(action.error)
};
case 'GET_USER':
return {
...state,
user: loadingState
};
case 'GET_USER_SUCCESS':
return {
...state,
user: success(action.data)
};
case 'GET_USER_ERROR':
return {
...state,
user: error(action.error)
};
default:
throw new Error(`Unhanded action type: ${action.type}`);
}
}
// State 용 Context와 Dispatch용 Context
const UsersStateContext = createContext(null);
const UsersDispatchContext = createContext(null);
// 위에서 선언한 두 가지 Context 들의 Provider로 감싸주는 컴포넌트
export const UsersProvider = ({ children }) => {
const [state, dispatch] = useReducer(usersReducer, initialState);
return (
<UsersStateContext.Provider value={state}>
<UsersDispatchContext.Provider value={dispatch}>
{children}
</UsersDispatchContext.Provider>
</UsersStateContext.Provider>
);
};
// State를 쉽게 조회할 수 있게 해주는 커스텀 Hook
export const useUsersState = () => {
const state = useContext(UsersStateContext);
if (!state) {
throw new Error("Cannot find UsersProvider");
}
return state;
};
//dispatch 를 쉽게 사용할 수 있게 해주는 hook
export const useUsersDispatch = () => {
const dispatch = useContext(UsersDispatchContext);
if (!dispatch) {
throw new Error("Cannot find UsersProvider");
}
return dispatch;
};
- App.js
import { UsersProvider } from "./components/UsersContext";
import Users from "./Users";
const App = () => {
return (
<UsersProvider>
<Users />
</UsersProvider>
);
};
export default App;
- Users.js
import { useState } from "react";
import {
useUsersState,
useUsersDispatch,
getUsers,
} from "./components/UsersContext";
import User from "./User";
const Users = () => {
// ======== 어떤 유저를 클릭했는지 세팅하여 그 유저의 데이터만 불러오게 하기 위한 값 ======== //
const [userId, setUserId] = useState(null);
const state = useUsersState();
const dispatch = useUsersDispatch();
//세팅했던 세 개의 값을 Context에서 불러옴
const { data: users, loading, error } = state.users;
const fetchData = () => {
getUsers(dispatch);
};
if (loading) return <div>loading...</div>;
if (error) return <div>에러가 발생했다</div>;
if (!users) return <button onClick={fetchData}>fetching</button>;
return (
<>
<ul>
{users.map((user) => (
<li
key={user.id}
onClick={() => setUserId(user.id)}
style={{ cursor: "pointer" }}
>
{/* {user.id} */}
{user.username}
</li>
))}
</ul>
<button onClick={fetchData}>다시 불러오기</button>
{userId && <User id={userId} />}
</>
);
};
export default Users;
- User.js
Context에 User를 세팅하면 값을 보여줄 컴포넌트
import React, { useEffect } from 'react';
import { useUsersState, useUsersDispatch, getUser } from './UsersContext';
function User({ id }) {
// ========= 이것 또한 Context의 User에서 값을 불러온다. ========== //
const state = useUsersState();
const dispatch = useUsersDispatch();
useEffect(() => {
getUser(dispatch, id);
}, [dispatch, id]);
const { data: user, loading, error } = state.user;
if (loading) return <div>로딩중..</div>;
if (error) return <div>에러가 발생했습니다</div>;
if (!user) return null;
return (
<div>
<h2>{user.username}</h2>
<p>
<b>Email:</b> {user.email}
</p>
</div>
);
}
export default User;
이렇듯 전역값으로 쓰일 데이터와 함수(dispatch)들을 따로 관리하여 Context로 분리하여 사용할 수 있다.
많이 사용되는 패턴이라 익숙해지는 것이 좋다.
리팩토링!
중복 되는 코드를 리팩토링.
일단 API 호출하는 함수는 따로 빼도록 한다.
- api.js
import axios from "axios";
export async function getUsers() {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
return response.data;
}
export async function getUser(id) {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${id}`
);
return response.data;
}
호출하는 함수와 리듀서 사용하는 함수의 공통적인 부분을 따로 빼어 리팩토링
함수를 만들어내는 함수를 따로 만들어준다.
- asyncActionUtils.js
// parameter : 액션의 타입, promise function
// ======= api 요청시 dispatch 함수를 만들어 주는 함수
/*
Context단에서
export const getUsers = createAsyncDispatcher("GET_USERS", api.getUsers);
와 같은 형태로 호출하여 함수를 만들고 있다.
*/
export const createAsyncDispatcher = (type, promiseFn) => {
const SUCCESS = `${type}_SUCCESS`;
const ERROR = `${type}_ERROR`;
// 새로운 함수를 만듭니다.
// ...rest 를 사용하여 나머지 파라미터를 rest 배열에 담습니다.
const actionHandler = async (dispatch, ...rest) => {
dispatch({ type }); // 요청 시작됨
try {
const data = await promiseFn(...rest);
dispatch({
type: SUCCESS,
data,
}); // 성공함
} catch (e) {
dispatch({
type: ERROR,
error: e,
}); // 실패함
}
};
return actionHandler; // 만든 함수를 반환합니다.
};
export const initialAsyncState = {
loading: false,
data: null,
error: null,
};
// 로딩중일 때 바뀔 상태 객체
const loadingState = {
loading: true,
data: null,
error: null,
};
// 성공했을 때의 상태 만들어주는 함수
const success = (data) => ({
loading: false,
data,
error: null,
});
// 실패했을 때의 상태 만들어주는 함수
const error = (error) => ({
loading: false,
data: null,
error: error,
});
// 세 가지 액션을 처리하는 리듀서를 만들어줌
// 기존에 중복되는 값들이 많았던 코드들을 함쳐줌!!
// type 은 액션 타입, key 는 리듀서에서 사용할 필드 이름
export function createAsyncHandler(type, key) {
// 성공, 실패에 대한 액션 타입 문자열을 준비합니다.
const SUCCESS = `${type}_SUCCESS`;
const ERROR = `${type}_ERROR`;
// 함수를 새로 만들어서
function handler(state, action) {
switch (action.type) {
case type:
return {
...state,
[key]: loadingState,
};
case SUCCESS:
return {
...state,
[key]: success(action.data),
};
case ERROR:
return {
...state,
[key]: error(action.error),
};
default:
return state;
}
}
// 반환합니다
return handler;
}
- UsersContext.js
//UsersContext의 기본 상태
const initialState = {
users: initialAsyncState,
user: initialAsyncState,
};
const usersHandler = createAsyncHandler("GET_USERS", "users");
const userHandler = createAsyncHandler("GET_USER", "user");
// ============= end states ============= //
// 위에서 만든 객체 / 유틸 함수들을 사용하여 리듀서 작성
const usersReducer = (state, action) => {
switch (action.type) {
case "GET_USERS":
case "GET_USERS_SUCCESS":
case "GET_USERS_ERROR":
return usersHandler(state, action);
case "GET_USER":
case "GET_USER_SUCCESS":
case "GET_USER_ERROR":
return userHandler(state, action);
default:
throw new Error(`Unhanded action type: ${action.type}`);
}
};
// State 용 Context와 Dispatch용 Context
const UsersStateContext = createContext(null);
const UsersDispatchContext = createContext(null);
export const getUsers = createAsyncDispatcher("GET_USERS", api.getUsers);
export const getUser = createAsyncDispatcher("GET_USER", api.getUser);
/* -> 기존코드
// export const getUsers = async (dispatch) => {
// dispatch({ type: "GET_USERS" });
// try {
// const response = await axios.get(
// "https://jsonplaceholder.typicode.com/users"
// );
// dispatch({
// type: "GET_USERS_SUCCESS",
// data: response.data,
// });
// } catch (err) {
// dispatch({
// type: "GET_USERS_ERROR",
// error: err,
// });
// }
// };
*/
// 위에서 선언한 두 가지 Context 들의 Provider로 감싸주는 컴포넌트
export const UsersProvider = ({ children }) => {
const [state, dispatch] = useReducer(usersReducer, initialState);
return (
<UsersStateContext.Provider value={state}>
<UsersDispatchContext.Provider value={dispatch}>
{children}
</UsersDispatchContext.Provider>
</UsersStateContext.Provider>
);
};
// State를 쉽게 조회할 수 있게 해주는 커스텀 Hook
export const useUsersState = () => {
const state = useContext(UsersStateContext);
if (!state) {
throw new Error("Cannot find UsersProvider");
}
return state;
};
//dispatch 를 쉽게 사용할 수 있게 해주는 hook
export const useUsersDispatch = () => {
const dispatch = useContext(UsersDispatchContext);
if (!dispatch) {
throw new Error("Cannot find UsersProvider");
}
return dispatch;
};
'Front-end > React 이론 스터디' 카테고리의 다른 글
Redux Middleware (0) | 2021.06.17 |
---|---|
Constate, Recoil (0) | 2021.06.11 |
CSS Module (0) | 2021.05.31 |
Context API (0) | 2021.05.29 |
useCallback (0) | 2021.05.26 |