리액트의 Context와 API연동을 함께 하고 싶을때, 어떤 작업을 해야하는 지 알아보자. 컴포넌트에서 필요한 외부 데이터들은 컴포넌트 내부에서 useAsync
같은 Hook을 사용해서 작업을 하면 충분하지만, 가끔씩 특정 데이터들은 다양한 컴포넌트에서 필요하게 될 때도 있다. 그럴 때에는 Context를 사용하면 편해진다.
src 디렉터리에서 UsersContext
파일을 만들어서 실습한다.
UsersContext에서 사용할 초기 상태를 선언한다.
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
});
위에서 만든 객체와 함수들을 이용해서 리듀서를 작성한다.
function 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(`Unhandled action type: ${action.type}`);
}
}
사용하게 쉽게 State와 Dispatch Context를 따로 만들기 -> 이렇게 해야 최적화를 할 수 있다
const UsersStateContext = createContext(null);
const UsersDispatchContext = createContext(null);
createContext를 만들었으면 Provider로 감싸주는 컴포넌트를 만들어야한다.
export function UsersProvider({children}){
const[state, dispatch] = useReducer(UsersReducer, initialState);
return(
<UsersStateContext.Provider value={state}>
<UsersDispatchContext.Provider value={dispatch}>
{children}
</UsersDispatchContext.Provider>
</UsersStateContext.Provider>
)
}
State,dispatch를 쉽게 조회할수 있는 커스텀 Hook만들기
export function useUsersState() {
const state = useContext(UsersStateContext);
if(!state){
throw new Error('Connot find UsersProvider');
}
return state;
}
export function useUserDispatch(){
const dispatch = useContext(UsersDispatchContext);
if(!dispatch){
throw new Error('Cannot find UsersProvider');
}
return dispatch;
}
API의 모든 User값을 가져오는 비동기함수와, 특정 Id값에 해당하는 user값을 가져오는 비동기함수를 따로 선언한다.
export async function getUsers(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(e){
dispatch({
type:'GET_USERS_ERROR',
error: e
})
}
}
export async function getUser(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
})
}
Users 컴포넌트의 내용중 위의 내용들과 중복되는 내용을 제거한다.
import React ,{useState} from "react";
import User from './User';
import { getUsers, useUsersDispatch, useUsersState } from "./UsersContext";
function Users() {
const [userId, setUserId] = useState(null);
const state = useUsersState();
const dispatch= useUsersDispatch();
const {loading, data:users, error} = state.users;
const fetchData =() => {
getUsers(dispatch);
}
if(loading) return <div>로딩중</div>
if(error) return <div>에러 발생</div>
if(!users) return <button onClick={fetchData}>불러오기</button>
return (
<>
<ul>
{users.map(user => (
<li key={user.id} onClick={() => setUserId(user.id)}>
({user.name})
</li>
))}
</ul>
<button onClick={fetchData}>다시 불러오기</button>
{userId && <User id={userId} /> }
</>
)
}
export default Users;
User컴포넌트도 이와같이 수정한다.
import React, {useEffect} from "react";
import { useUsersState, useUsersDispatch, getUser } from "./UsersContext";
function User({id}) {
const state = useUsersState();
const dispatch = useUsersDispatch();
useEffect(() => {
getUser(dispatch, id);
},[dispatch,id]);
const { loading, data:user, 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;
App에서도 provider로 Users컴포넌트를 감싼다.
import "./App.css";
import Users from "./Users";
import { UsersProvider } from "./UsersContext";
function App() {
return (
<UsersProvider>
<Users />
</UsersProvider>
)
}
export default App;
이전의 결과화면과 동일하게 동작한다.
하지만 UsersContext내부의 코드 내용에서는 중복된 내용이 여러개 있기 때문에 약간의 리팩토링을 하고자 한다.
api.js
를 만들어서 단순히 api 함수를 호출해서 데이터를 반환하게 할것이다.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;
}
다음 asyncActionUtil.js
파일을 생성한다. 여기서는 createAsyncDispatcher
함수를 작성하는데, 이 함수는 UsersContext
에서 작성했던 getUsers
나 getUser
같은 비동기 함수를 쉽게 작성할 수 있게 해주는 함수이다. createAsyncDispatch
에서 받아온 type
를 가져와서 데이터 요청에 성공한 경우와, 실패로 인한 에러가 발생하는 경우에 대한 코드를 작성한다.
export default function createAsyncDispatcher(type,promiseFn) {
const SUCCESS = `${type}_SUCCESS`;
const ERROR =`${type}_ERROR`;
async function actionHandler(dispatch, ...rest){
dispatch({type});
try{
const data = await promiseFn(...rest);
dispatch({
type:SUCCESS,
data
});
} catch (e) {
dispatch({
type: ERROR,
error: e
});
}
}
return actionHandler;
}
리턴되는 actionHandler
를 사용해서 UsersContext
의 코드를 줄여보자. 비동기 함수로 작성했던 getUsers
와 getUser
를 제거하고 api
와 asyncActionUtils
import 해서 필요한 코드를 2줄로 줄여버렸다.
export const getUsers = createAsyncDispatcher('GET_USERS', api.getUsers);
export const getUser = createAsyncDispatcher('GET_USER', api.getUser);
이전에 작성한 함수가 이랬다.
export async function getUsers(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(e){
dispatch({
type:'GET_USERS_ERROR',
error: e
})
}
}
export async function getUser(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
})
}
이 장문의 코드를 2줄로 줄인 것이다.
UsersContext 내부에서 일련의 상태를 생성하는 객체/함수가 있었는데(loadingState, success, error) 이들을 전부 잘라내서 asyncActionUtils
로 옮겨서 수정할 것이다.
asyncActionUtils
에서 initialAsyncState라는 객체를 만들어서 UsersContext
내부에 초기 객체를 이 객체로 수정한다.
export const initialAsyncState = {
loading: false,
data: null,
error: null
}
UsersContext
const initialState = {
users : initialAsyncState,
user : initialAsyncState
};
createAsyncHandler
를 작성한다.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;
}
UserContext
에서 UsersReducer
의 내부 코드를 수정한다. 먼저 이전에 작성했던 crateAsyncHandler
를 불러와서 사용한다. users의 상태, user상태 각각 상태를 관리하기 위한 Handler를 생성한다.const usersHandler = createAsyncHandler('GET_USERS', 'users');
const userHandler = createAsyncHandler('GET_USER', 'user');
UsersReducer
내부를 수정한다.function 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(`Unhandled action type: ${action.type}`);
}
}
switch문의 case 마다 하나하나 다 작성한 것 대신에 단 두줄로 상황별로 상태를 관리할 수 있게 되었다.