19_리액트 API연동
FrontEnd/React

19_리액트 API연동

728x90

컴포넌트에서 API를 연동하는 방법에 대해 다뤄보겠다. 리덕스를 사용하지는 않겠다.

 

Promise, async/await을 사용하여 구현해보겠다!

 

리액트 프로젝트를 하나만들고

 

yarn add axios

axios 라이브러리를 설치해준다.

 

REST API

클라이언트-서버가 소통하는 방식

 

GET

POST

PUT

DELETE

 

예를들어

GET /users/1

이라고하면 아이디가 1인 계정을 찾아 반환하게 되고

POST/articles

라고 하면 기사글을 등록할 수 있는 API이다.

 

axios 라이브러리를 사용하면 위 기능을 쉽게 사용할 수 있다.

 

import axios from 'axios';

axios.get('/users/1');

 

https://jsonplaceholder.typicode.com/

 

JSONPlaceholder - Free Fake REST API

{JSON} Placeholder Free fake API for testing and prototyping. Powered by JSON Server + LowDB As of Oct 2021, serving ~1.7 billion requests each month.

jsonplaceholder.typicode.com

또한 JSONPlacholder에서 제공하는 연습용 API를 가지고 구현할 것이다.

 

그중에서도 users API를 가지고 실습해볼 것이다.

 

https://jsonplaceholder.typicode.com/users

 

 

API를 관리할때는 3가지 상태를 관리하게 된다.

 

1. 요청의 결과

2. 로딩 상태

3. 에러

 

 

import React ,{useState,useEffect}from 'react';
import axios from 'axios';


function Users() {

    const [users,setUsers] = useState(null);   //결과값
    const [loading,setloading] = useState(false); // 로딩되는지 여부
    const [error,setError] = useState(null); //에러

    useEffect( () =>{
        const fetchUsers = async () => {
            try {

            } catch (e) {
                
            }
        }
    },[] )


    return (
        <div>
            
        </div>
    );
    
}


export default Users;

3가지 기능을 구현하고 랜더링될때 값을 받아오기 위해서 위처럼 기본 틀을 잡아준다.

 

import React ,{useState,useEffect}from 'react';
import axios from 'axios';


function Users() {

    const [users,setUsers] = useState(null);   //결과값
    const [loading,setLoading] = useState(false); // 로딩되는지 여부
    const [error,setError] = useState(null); //에러

    const fetchUsers = async () => {
        try {
            setUsers(null);
            setError(null);
            setLoading(true); //로딩이 시작됨
            const response = await axios.get('https://jsonplaceholder.typicode.com/users/');
            setUsers(response.data);
        } catch (e) {
            setError(e);
        }
        setLoading(false);
    };



    useEffect( () =>{
        
        fetchUsers();
    },[] )


    if ( loading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!users) return null;  //users값이 유효하지 않는 경우

    return (
        <>        
            <ul>
                {users.map(user => <li key={user.id}>
                    {user.username} ({user.name})
                </li>)}
            </ul>
            <button onClick={fetchUsers}>다시 불러오기</button>
        </>

    );
    
}


export default Users;

get을 가져와서 서버에서 값을 가져왔다. 버튼도 하나 추가해줘서 가져오는 작업을 하게 했다.

 

 

이전에 배운 useReducer를 사용하면 useState를 굳이 3번 쓰지 않아도 된다.

더보기
import React ,{useEffect, useReducer}from 'react';
import axios from 'axios';


function reducer(state,action) {
    switch (action.type) {
        case 'LOADING':
            return {
                loading : true,
                data : null,
                error : null,
            };
        case 'SUCCESS':
            return {
                loading : false,
                data : action.data,
                error : null,
            };
        case 'ERROR':
            return {
                loading : false,
                data : null,
                error : action.error,
            };
        default:
            throw new Error('Error !!!!!!!!!!')
    }
}


function Users() {

    const [state,dispatch] = useReducer(reducer, {
        loading : false,
        data : null,
        error : null,
    });

    const fetchUsers = async () => {

        dispatch ({ type : 'LOADING'});

        try {
            
            const response = await axios.get('https://jsonplaceholder.typicode.com/users/');
            dispatch({type : 'SUCCESS', data : response.data});                       

        } catch (e) {
            console.log(e.response.status);
            dispatch ({ type : 'ERROR', error});
        }
        
    };



    useEffect( () =>{
        
        fetchUsers();
    },[] )

    const { loading,error, data : users } = state;  //data값이 users로 들어감
    if ( loading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!users) return null;  //users값이 유효하지 않는 경우

    return (
        <>        
            <ul>
                {users.map(user => <li key={user.id}>
                    {user.username} ({user.name})
                </li>)}
            </ul>
            <button onClick={fetchUsers}>다시 불러오기</button>
        </>

    );
    
}


export default Users;

 

 

 

useAsync 커스텀 Hook 이용

 

함수 부분을 우클릭하여 리팩터링 기능을 사용하면 함수를 따로 빼내는것도 쉽게할 수 있다. (VSCODE 기능)

 

 

더보기
//useAsync.js
import { useReducer,useEffect, useCallback } from "react";

function reducer(state,action) {
    switch (action.type) {
        case 'LOADING':
            return {
                loading : true,
                data : null,
                error : null,
            };
        case 'SUCCESS':
            return {
                loading : false,
                data : action.data,
                error : null,
            };
        case 'ERROR':
            return {
                loading : false,
                data : null,
                error : action.error,
            };
        default:
            throw new Error('Error !!!!!!!!!!')
    }
}

// deps : useEffect를 사용할때 두번째 파라미터 값을 받아와서 그대로 사용
function useAsync(callback,deps = []) {
    const [state,dispatch] = useReducer(reducer, {
        loading : false,
        data : null,
        error : null,
    });
    

    //useCallback은 생략 가능
    const fetchData = useCallback(async () => {
        dispatch( {type : 'LOADING'});
        try {
            const data = await callback();
            dispatch({ type : 'SUCCESS', data});
        } catch (e) {
            dispatch({ type : 'ERROR', error : e})
        }
    }, [callback]);


    useEffect(() => {
        fetchData();
        // eslint-disable-next-line
    },deps);

    return [state,fetchData];
}


export default useAsync;

 

//Users.js
import React from 'react';
import axios from 'axios';
import useAsync from './useAsync';


async function getUsers() {
    const response = await axios.get('https://jsonplaceholder.typicode.com/users/');
    return response.data;
}


function Users() {

    
    const [state,refetch] = useAsync(getUsers,[]);

    const { loading,error, data : users } = state;  //data값이 users로 들어감
    if ( loading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!users) return null;  //users값이 유효하지 않는 경우

    return (
        <>        
            <ul>
                {users.map(user => <li key={user.id}>
                    {user.username} ({user.name})
                </li>)}
            </ul>
            <button onClick={refetch}>다시 불러오기</button>
        </>

    );
    
}


export default Users;

위와같이 2개로 분리하여 나타낼 수 있다.

 

이경우 액션 없이 바로 정보를 불러오는데, 사용자 액션이 있을때만 정보를 불러오고 싶게 변경하고 싶다면

 

function useAsync(callback,deps = [],skip = false) {
    const [state,dispatch] = useReducer(reducer, {
        loading : false,
        data : null,
        error : null,
    });
    

    //useCallback은 생략 가능
    const fetchData = useCallback(async () => {
        dispatch( {type : 'LOADING'});
        try {
            const data = await callback();
            dispatch({ type : 'SUCCESS', data});
        } catch (e) {
            dispatch({ type : 'ERROR', error : e})
        }
    }, [callback]);


    useEffect(() => {
        if (skip) return;

        fetchData();
        // eslint-disable-next-line
    },deps);

    return [state,fetchData];
}

다음처럼 3번째 파라미터로 skip을 추가해준후 skip이 true이면 바로 반환해주는 코드를 추가해준다.

 

const [state,refetch] = useAsync(getUsers,[], true);

    const { loading,error, data : users } = state;  //data값이 users로 들어감
    if ( loading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!users) return <button onClick={refetch}>불러오기</button>;  //users값이 유효하지 않는 경우

Users.js에서 해당부분을 버튼으로 바꾸어주면 될것 같다.

 

 

 

 

불러오기 버튼을 눌러야만 실행됨을 볼 수 있다.

 

 

 

user id를 가지고 검색하기

 

주소 뒤에 /id 값을 붙이면 해당 id의 정보값만 나오게 된다.

이를 활용해서 해당 id값만 나오게 추출해보자.

 

//Users.js

import React from 'react';
import axios from 'axios';
import useAsync from './useAsync';

async function getUsers({id}) {
    const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`)
    return response.data;
}

function User( id ) {


    const [state] = useAsync(() => getUsers(id),[id]);  //id가 바뀔때마다 호출   

    const { loading,error, data : users } = state;  //data값이 users로 들어감
    if ( loading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!users) return null;  //users값이 유효하지 않는 경우


    return (
        <div>
            <h2>{users.username}</h2>
            <p>
                <b>Email : </b> {users.email}
            </p>
        </div>
    );
    
}


export default User;

이전에 만든 Hook을 이용해서 만들었다. Users.js와 굉장히 유사하지만 id값을 파라미터로 받아와서 관리하는것이 다르다.

 

function Users() {

    
    const [state,refetch] = useAsync(getUsers,[], true);
    const [userId,setUserId] = useState(null);  //개개인 id로 호출하기 위한 useState

    const { loading,error, data : users } = state;  //data값이 users로 들어감
    if ( loading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!users) return <button onClick={refetch}>불러오기</button>;  //users값이 유효하지 않는 경우

    return (
        <>        
            <ul>
                {users.map(user => <li key={user.id} onClick={() => setUserId(user.id)}>
                    {user.username} ({user.name})
                </li>)}
            </ul>
            <button onClick={refetch}>다시 불러오기</button>
            {userId && <User id = {userId} /> }
        </>

    );
    
}

Users.js 내부에 useState를 사용하여 Id값을 관리하는 변수를 하나 관리해주고 id값이 있을때 그 관리값을 보여주는 코드를 작성하면

 

위처럼 해당 id값을 조회할 수 있다.

 

 

 

react-async

 

직접 Hook을 만들기 싫다면 위 라이브러리를 사용해도 좋다

 

yarn add react-async

우선 라이브러리를 설치해 준다.

 

https://github.com/ghengeveld/react-async

 

GitHub - async-library/react-async: 🍾 Flexible promise-based React data loader

🍾 Flexible promise-based React data loader. Contribute to async-library/react-async development by creating an account on GitHub.

github.com

라이브러리 안의 정보를 알고싶다면 위 github링크를 참조하자.

 

더보기
//Users.js

import React from 'react';
import axios from 'axios';
import { useAsync } from 'react-async'

async function getUser({id}) {
    const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`)
    return response.data;
}

function User( {id} ) {

    const {
        data : user,
        error,
        isLoading
    } = useAsync({
        promiseFn : getUser,
        id,
        watch : id, // id 값이 바뀌면 다시 실행하겠다. 기존 deps와 유사
    });

    

    
    if ( isLoading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!user) return null;  //users값이 유효하지 않는 경우


    return (
        <div>
            <h2>{user.username}</h2>
            <p>
                <b>Email : </b> {user.email}
            </p>
        </div>
    );
    
}


export default User;

 

 

//Users.js
import React ,{useState} from 'react';
import axios from 'axios';
import {useAsync} from 'react-async';
import User from './User'


async function getUsers() {
    const response = await axios.get('https://jsonplaceholder.typicode.com/users/');
    return response.data;
}


function Users() {

    const [userId,setUserId] = useState(null);  //개개인 id로 호출하기 위한 useState
    const {data : users,error,isLoading,reload, run} = useAsync({  //reload : 기존 refetch
        deferFn : getUsers  //시작될때 아무것도 안하기 위함
    })

    
    if ( isLoading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!users) return <button onClick={run}>불러오기</button>;  //users값이 유효하지 않는 경우

    return (
        <>        
            <ul>
                {users.map(user => <li key={user.id} onClick={() => setUserId(user.id)}>
                    {user.username} ({user.name})
                </li>)}
            </ul>
            <button onClick={reload}>다시 불러오기</button>
            {userId && <User id = {userId} /> }
        </>

    );
    
}


export default Users;

위에서 했던 똑같은 기능을 라이브러리로 사용해서 구현하였다. 기존과 차이점은 별로 없고, deferFn은 시작할때 안보여주게, promiseFn을 사용하면 새로고침과 동시에 랜더링이 됨을 알 수 있다.

 

라이브러리를 통해 쉽게 기능들을 쓸 수 있는 장점이 있지만 옵션이 복잡하고 조금 어렵다는 단점이 있다!

 

 

 

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

 

특정 데이터가 다양한 컴포넌트에서 필요한 경우 데이터를 Context에 넣어주면 편리하다.

 

Context를 사용하면 여러 컴포넌트에서 전역변수처럼 코드들을 재사용할 수 있다고 저번 글에서 설명했었다. 이를 이용해서 똑같은 작업을 Context상에서 구현하고 불러오게 구현해보았다.

 

아래는 풀 코드이다.

 

더보기
//UsersContext.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,
});

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('Error!!!!');
    }
}




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('Error!!!! _UserProvider');
    }
    return state;
}

export function useUsersDispatch() {
    const dispatch = useContext(UsersDispatchContext);
    if(!dispatch){
        throw new Error('Error!!!! _UserProvider');
    }
    return dispatch;
}


export async function getUsers(dispatch){   //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 { getUsers, useUsersDispatch, useUsersState } from './UsersContext';




function Users() {

    const [userId,setUserId] = useState(null);  //개개인 id로 호출하기 위한 useState
    const state = useUsersState();
    const dispatch = useUsersDispatch();

    const { isLoading,data : users , error } = state.users;

    const fetchData = () =>{
        getUsers(dispatch);
    }


    if ( isLoading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!users) return <button onClick={fetchData}>불러오기</button>;  //users값이 유효하지 않는 경우

    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();

    const { isLoading,data : user , error } = state.user;
    

    useEffect(() => {
        getUser(dispatch,id);
    },[dispatch,id])
    
    if ( isLoading ) return <div>로딩중..</div>
    if (error) return <div>에러 발생!!</div>
    if (!user) return null;  //users값이 유효하지 않는 경우


    return (
        <div>
            <h2>{user.username}</h2>
            <p>
                <b>Email : </b> {user.email}
            </p>
        </div>
    );
    
}


export default User;

 

Context를 이용하여 구현해보았다. 조금 어려울 수 있겠지만

 

결국 뿌려져 있던 기능들을 Context파일로 합쳤고, 이를 불러내서 사용할수 있는 코드이다.

위코드는 재사용될법한 코드가 정말 많은데 이를 한번 정리해 보겠다.

 

리팩토링

 

재사용을 줄이기 위해서 생성하기 편한 유틸함수들을 먼저 만들어서 활용하였다.

 

//asyncActionUtils.js

export default function createAsyncDispatcher(type,promisFn) {
    const SUCCESS = `${type}_SUCCESS`;
    const ERROR =  `${type}_ERROR`;

    async function actionHandler(dispatch,...rest) {
        dispatch({type});
        try {
            const data = await promisFn(...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 = e => ({
    loading : false,
    data : null,
    error : e,
});


export function createAsyncHandler(type,key) {   //type : action.type , key :user,users와같은 특정 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
}

 

 

아래는 단순 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;
}

 

 

이렇게 함수를 유틸성있게 만들어두면 원래 길고 복잡했던 코드를 줄일 수 있고, 비슷한 함수들을 추가할때도 엄청 편리하게 추가할 수있다.

 

 

아래는 위 코드로 줄인 코드의 모습이다.

//UsersContext.js
import React, {createContext,useReducer,useContext} from 'react';
import * as api from './api'; //api안의 함수를 다 데리고옴
import createAsyncDispatcher, { createAsyncHandler, initialAsyncState } from './asyncActionUtils';


const usersHandler = createAsyncHandler('GET_USERS','users');
const userHandler = createAsyncHandler('GET_USER','user');



const initialState = {
    users : initialAsyncState,
    user : initialAsyncState
}




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('Error!!!!');
    }
}




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('Error!!!! _UserProvider');
    }
    return state;
}

export function useUsersDispatch() {
    const dispatch = useContext(UsersDispatchContext);
    if(!dispatch){
        throw new Error('Error!!!! _UserProvider');
    }
    return dispatch;
}


export const getUsers = createAsyncDispatcher('GET_USERS',api.getUsers);
export const getUser = createAsyncDispatcher('GET_USER',api.getUser);

 

까다롭거나 큰 프로젝트에서는 Redux나 리덕스 미들웨어를 사용하는것이 엄청 효율적이다. 이에 대해서는 공부한 후에 또 글을 적도록 하겠다.

728x90

'FrontEnd > React' 카테고리의 다른 글

21_리액트_리덕스  (0) 2022.01.02
20_리액트_라우터  (0) 2021.12.30
18_styled-components  (0) 2021.12.27
17_리액트_CSS Module  (0) 2021.12.24
16_리액트_컴포넌트스타일링  (0) 2021.12.24