Context에서 비동기 작업 상태 관리하기
2020-07-11

특정 컴포넌트에서 필요한 외부데이터는 컴포넌트 내부에서 useAsync 같은 hook을 사용하면 충분하지만 특정 데이터가 다양한 컴포넌트에서 필요한 경우가 있다. 예를 들어 로그인 상태같은 경우 이다. 여기저기서 필요한 상태를 비동기적으로 가져와 사용해야 할 경우 데이터를 context에 넣어 관리하는 것이 편하다. context 내부에서 비동기작업을 위한 상태 관리를 한다면 어떻게 한다면 효율적인지 알아보자.

UsesContext.js

import React, {createContext, useReducer, useContext} from 'react';
import axios from 'axios';

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 = e => ({
    loading : false,
    data: null,
    error: e
});

// GET_USERS
// GET_USERS_SUCCESS
// GET_USERS_ERROR
// GET_USER
// GET_USER_SUCCESS
// GET_USER_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:
            return null;
    }
}

const UsersStateContext = createContext(null);
const UsersDispatchContext = createContext(null);

export function UsersProvider({children}){
    const [state, dispatch] = useReducer(usersReducer, initialState);
    return (
        <UsersStateContext.Provider value={state}>
            <UsersDispatchContext.Provider value={dispatch}>
                {children}
            </UsersDispatchContext.Provider>
        </UsersStateContext.Provider>
    );
}

export function useUsersState() {
    const state = useContext(UsersStateContext);
    if(!state){
        throw new Error("Cannot find UserProvier");
    }
    return state;
}

export function useUsersDispatch() {
    const dispatch = useContext(UsersDispatchContext);
    if(!dispatch){
        throw new Error("Cannot find UsersDispatchContext");
    }
    return dispatch;
}

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.js

import React, {useState} from 'react';
import User from "./User";
import {useUsersDispatch, useUsersState, getUsers} 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.username} ({user.name})
                    </li>
                ))}
            </ul>
            <button onClick={fetchData}>다시 불러오기</button>
            {userId && <User id={userId}/>}
        </>
    );
}

export default Users;

User.js

import React, {useEffect} from 'react';
import {useUsersDispatch, useUsersState, 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;
© 2020, Built with Gatsby, React, Typscript, styled-components