본문 바로가기

React/API 연동

[React] #6 | Context 와 함께 사용하기

리액트의 Context와 API연동을 함께 하고 싶다면 어떻게 해야 되는지 알아보자. ( + 코드를 함수화해서 재사용하는 것 까지! )

 

컴포넌트에서 필요한 외부 데이터들은 컴포넌트 내부에서 useAsync와 같은 Hook을 사용해서 작업하면 충분하다!

 

하지만, 가끔씩 특정 데이터들은 다양한 컴포넌트에서 필요하게 될때도 있는데 그럴때에는 Context를 사용하면 개발이 편해진다!

예를 들면, 현재 로그인된 사용자의 정보, 설정

 


1️⃣ Context 준비하기

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

// 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('Unhanded action type: ${action.type}');
    }
}

// State 용 Context와 Dispatch 용 Context 따로 만들어주기
const UsersStateContext = createContext(null);
const UsersDispatchContext = createContext(null);

// 위에서 선언한 두가지 Context 들의 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를 쉽게 조회할 수 있게 해주는 커스텀 Hook
export function useUsersState(){
    const state = useContext(UsersStateContext);
    if(!state){
        throw new Error('Cannot find UsersProvider');
    }
    return state;
}

// Dispatch를 쉽게 조회할 수 있게 해주는 커스텀 Hook
export function useUsersDispatch(){
    const dispatch = useContext(UsersDispatchContext);
    if(!dispatch){
        throw new Error('Cannot find UsersProvider');
    }
    return dispatch;
}

 

 

➰위의 액션 디스패치 사용법

  • 요청이 시작했을 때 액션을 디스패치해주고, 요청이 성공하거나 실패했을 때 또 다시 디스패치 해준다.
dispatchEvent({type: 'GET_USER'});
try{
  const response = await getUsers();
  dispatch({type:'GET_USER_SUCCESS', data: response.data});  
} catch(e){
  dispatch({type: 'GET_USER_ERROR', error: e });
}

 

2️⃣ API 처리 함수 만들기

이제 이러한 작업을 처리하는 함수 ( = API 처리 함수 )를 만들기!

UserContext.js 를 열어서 상단에 axios를 불러오고, 코드의 하단 부분에 getUsersgetUser 함수를 작성!

이 함수들은 dispatch를 파라미터로 받아오고, API에 필요한 파라미터도 받아오게 된다.

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

//..........
//..........

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

중복되는 코드들은 나중에 리펙토링!

 

3️⃣ Context 사용하기

App 컴포넌트를 열어서 UsersProvider로 감싸기

 

App.js

import React from 'react';
import Users from './Users';
import { UsersProvider } from './UsersContext';

function App() {
  return (
    <UsersProvider>
      <Users />
    </UsersProvider>
  );
}

export default App;

이제, Users 컴포넌트의 코드를 Context를 사용하는 형태의 코드로 전환해보기

 

Users.js

import React, { useState } from 'react';
import { useUsersState, useUsersDispatch, getUsers } from './UsersContext';
import User from './User';

function Users() {
  const [userId, setUserId] = useState(null);

  // state와 dispatch 가져오기
  const state = useUsersState();
  const dispatch = useUsersDispatch();

  const { data: users, loading, error } = state.users;
  const fetchData = () => {
    // 요청을 시작할 때 getUsers 함수 안에 dispatch를 넣어서 호출해주기
    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)}
            style={{ cursor: 'pointer' }}
          >
            {user.username} ({user.name})
          </li>
        ))}
      </ul>
      <button onClick={fetchData}>다시 불러오기</button>
      {userId && <User id={userId} />}
    </>
  );
}

export default Users;

 

다음, User 컴포넌트도 전환해보기

 

User.js

import React, {useEffect} from 'react';
import { useUsersState, useUsersDispatch, getUser } from './UsersContext';

function User({ id }){

    // useUsersState()로 state 불러오기
    const state = useUsersState();
    // useUsersDispatch()로 dispatch 불러오기
    const dispatch = useUsersDispatch();

    // useEffect함수를 사용해서, id 값이 바뀔 때마다 getUser()함수를 호출!
    // getUser()함수를 호출 할 때에는 두번째 파라미터에 현재 props에서 받아온 id 값을 넣어줌.
    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;

 

4️⃣ 반복되는 코드 줄이기

지금까지 배운 것은, Context + 비동기 API 연동의 정석!

앞으로 이 패턴을 잘 활용하기!

 

이제 여기서 조금 더 나아가서, 반복되는 로직들을 함수화하여 재활용하는 방법을 알아보자!

 

반복된 코드 (리펙토링 해볼 부분)

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

 

우선, api 들이 들어있는 파일을 따로 분리해주기!

src 디렉터리에 api.js 파일을 만들고 아래와 같이 코드를 적어준다.

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 라는 파일을 만들고 아래와 같은 코드를 작성해주자

asyncActionUtils.js

// 이 함수는 파라미터로 액션의 타입 (예: GET_USER) 과 Promise 를 만들어주는 함수를 받아옵니다.
export default function createAsyncDispatcher(type, promiseFn) {
  // 성공, 실패에 대한 액션 타입 문자열을 준비합니다.
  const SUCCESS = `${type}_SUCCESS`;
  const ERROR = `${type}_ERROR`;

  // 새로운 함수를 만듭니다.
  // ...rest 를 사용하여 나머지 파라미터를 rest 배열에 담습니다.
  async function actionHandler(dispatch, ...rest) {
    dispatch({ type }); // 요청 시작됨
    try {
      const data = await promiseFn(...rest); // rest 배열을 spread 로 넣어줍니다.
      dispatch({
        type: SUCCESS,
        data
      }); // 성공함
    } catch (e) {
      dispatch({
        type: ERROR,
        error: e
      }); // 실패함
    }
  }

  return actionHandler; // 만든 함수를 반환합니다.
}
  • 이렇게 createAsyncDispatcher 를 만들어주면, UsersContext 의 코드를 다음과 같이 리펙토링 할 수 있다.

 

UsersContext.js

import React, { createContext, useReducer, useContext } from 'react';
import createAsyncDispatcher from './asyncActionUtils';
import * as api from './api'; // api 파일에서 내보낸 모든 함수들을 불러옴

(...)

export const getUsers = createAsyncDispatcher('GET_USERS', api.getUsers);
export const getUser = createAsyncDispatcher('GET_USER', api.getUser);
  • api 파일에서 getUsersgetUser을 불러와서
  • asyncActionUtils.js 파일에서 만든 함수 createAsyncDispatcher함수를 타입과 함께 적용해준다.

 

 

 

➕ 리듀서쪽 코드도 리팩토링!

  • UsersContext 의 loadingState, success, error를 잘라내서 asyncActionUtils.js 안에 붙여넣기
  • 그리고, initialAsyncState 객체를 만들어서 내보내고, createAsyncHandler 라는 함수도 만들어서 내보내기
  • 모두 그냥 asyncActionUtils.js 파일에 넣어버령!!!

asyncActionUtils.js

// 이 함수는 파라미터로 액션의 타입 (예: GET_USER) 과 Promise 를 만들어주는 함수를 받아옵니다.
export function createAsyncDispatcher(type, promiseFn) {
  // 성공, 실패에 대한 액션 타입 문자열을 준비합니다.
  const SUCCESS = `${type}_SUCCESS`;
  const ERROR = `${type}_ERROR`;

  // 새로운 함수를 만듭니다.
  // ...rest 를 사용하여 나머지 파라미터를 rest 배열에 담습니다.
  async function actionHandler(dispatch, ...rest) {
    dispatch({ type }); // 요청 시작됨
    try {
      const data = await promiseFn(...rest); // rest 배열을 spread 로 넣어줍니다.
      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 는 리듀서서 사용할 필드 이름입니다 (예: user, users)
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 에서, 방금 만든 initialAsyncStatecreateAsyncHandler를 사용해서 코드를 고쳐보자

 

UsersContext.js

import React, { createContext, useReducer, useContext } from 'react';
import {
  createAsyncDispatcher,
  createAsyncHandler,
  initialAsyncState
} from './asyncActionUtils';
import * as api from './api'; // api 파일에서 내보낸 모든 함수들을 불러옴

// UsersContext 에서 사용 할 기본 상태
const initialState = {
  users: initialAsyncState,
  user: initialAsyncState
};

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

// 위에서 만든 객체 / 유틸 함수들을 사용하여 리듀서 작성
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(`Unhanded action type: ${action.type}`);
  }
}

(...)
  • 위의 각 switch 문에서, 만약 return 또는 break를 하지 않으면, 여러개의 case에 대해 동일한 코드를 실행해주게 된다.
  • 예를 들면, GET_USERS, GET_USERS_SUCCESS, GET_USERS_ERROR 액션이 발생하게 된다면 usersHandler(state, action)을 호출해서 반환을 해준다.

 

꼭 이렇게 까지 정리할 필요는 없지만,, 자주 사용되는 코드를 함수화해서 재사용하면 좋다!

 


Reference

벨로퍼트의 모던 리액트