Context에서 비동기작업 상태 관리하기

정영찬·2022년 3월 7일
0

리액트

목록 보기
43/79
post-thumbnail

리액트의 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내부의 코드 내용에서는 중복된 내용이 여러개 있기 때문에 약간의 리팩토링을 하고자 한다.

  • getUsers, getUser 비동기 함수의 코드 내용이 거의 유사하므로 깔끔한 코드를 위해 수정한다.
    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에서 작성했던 getUsersgetUser 같은 비동기 함수를 쉽게 작성할 수 있게 해주는 함수이다. 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의 코드를 줄여보자. 비동기 함수로 작성했던 getUsersgetUser를 제거하고 apiasyncActionUtils 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 마다 하나하나 다 작성한 것 대신에 단 두줄로 상황별로 상태를 관리할 수 있게 되었다.

profile
개발자 꿈나무

0개의 댓글