API Fetching을 Context와 함께 써보자.

Front-end/React 이론 스터디

API Fetching을 Context와 함께 써보자.

조커린 2021. 6. 4. 00:02

01. API Fetching & Context API

데이터 패칭시 Reducer 와 Context를 함께 써보자

데이터 패칭 시 Reducer와 Context를 함께 쓰면 더 좋을 때가 있다.

외부 데이터들 같은 경우는 그 때 그 때 외부 API를 요청하면 되지만, 사이트 전역적으로 쓰일 데이터 ( 예를 들면 유저 데이터) 와 같은 것들은 Context와 함께 연동해서 쓰면 더욱 좋다.

Context 코드를 따로 분리하여 Provider, State, dispatch를 가져다 쓸 수 있는 함수와 Reducer함수, state등을 따로 분리한다.

  • UsersContext.js

→ getUsers, getUser ( data fetching )

→ reducer, initial state, loading State, error state

→ provider, useUsersState(Hook), useUsersDispatch(Hook)

//================== 데이터 패칭 함수 ===================//

export const getUsers = async (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 (err) {
    dispatch({
      type: "GET_USERS_ERROR",
      error: err,
    });
  }
};

export const getUser = async (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 });
  }
}

//================== 상태값 세팅 ===================//
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
});

//================== 리듀서 함수 ===================//
const 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 const 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 const useUsersState = () => {
  const state = useContext(UsersStateContext);
  if (!state) {
    throw new Error("Cannot find UsersProvider");
  }
  return state;
};
//dispatch 를 쉽게 사용할 수 있게 해주는 hook
export const useUsersDispatch = () => {
  const dispatch = useContext(UsersDispatchContext);
  if (!dispatch) {
    throw new Error("Cannot find UsersProvider");
  }
  return dispatch;
};
  • App.js
import { UsersProvider } from "./components/UsersContext";
import Users from "./Users";

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

export default App;
  • Users.js
import { useState } from "react";
import {
  useUsersState,
  useUsersDispatch,
  getUsers,
} from "./components/UsersContext";
import User from "./User";

const Users = () => {
// ======== 어떤 유저를 클릭했는지 세팅하여 그 유저의 데이터만 불러오게 하기 위한 값 ======== //
  const [userId, setUserId] = useState(null); 
  const state = useUsersState();
  const dispatch = useUsersDispatch();

  //세팅했던 세 개의 값을 Context에서 불러옴
  const { data: users, loading, error } = state.users;
  const fetchData = () => {
    getUsers(dispatch);
  };

  if (loading) return <div>loading...</div>;
  if (error) return <div>에러가 발생했다</div>;
  if (!users) return <button onClick={fetchData}>fetching</button>;

  return (
    <>
      <ul>
        {users.map((user) => (
          <li
            key={user.id}
            onClick={() => setUserId(user.id)}
            style={{ cursor: "pointer" }}
          >
            {/* {user.id} */}
            {user.username}
          </li>
        ))}
      </ul>
      <button onClick={fetchData}>다시 불러오기</button>
      {userId && <User id={userId} />}
    </>
  );
};

export default Users;
  • User.js

Context에 User를 세팅하면 값을 보여줄 컴포넌트

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

function User({ id }) {
    // ========= 이것 또한 Context의 User에서 값을 불러온다. ========== //
  const state = useUsersState();
  const dispatch = useUsersDispatch();

  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;

이렇듯 전역값으로 쓰일 데이터와 함수(dispatch)들을 따로 관리하여 Context로 분리하여 사용할 수 있다.
많이 사용되는 패턴이라 익숙해지는 것이 좋다.

리팩토링!

중복 되는 코드를 리팩토링.
일단 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;
}

호출하는 함수와 리듀서 사용하는 함수의 공통적인 부분을 따로 빼어 리팩토링
함수를 만들어내는 함수를 따로 만들어준다.

  • asyncActionUtils.js
// parameter : 액션의 타입, promise function
// ======= api 요청시 dispatch 함수를 만들어 주는 함수
/*
Context단에서 
export const getUsers = createAsyncDispatcher("GET_USERS", api.getUsers);
와 같은 형태로 호출하여 함수를 만들고 있다. 
*/
export const createAsyncDispatcher = (type, promiseFn) => {
  const SUCCESS = `${type}_SUCCESS`;
  const ERROR = `${type}_ERROR`;

  // 새로운 함수를 만듭니다.
  // ...rest 를 사용하여 나머지 파라미터를 rest 배열에 담습니다.
  const actionHandler = async (dispatch, ...rest) => {
    dispatch({ type }); // 요청 시작됨
    try {
      const data = await promiseFn(...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 = (error) => ({
  loading: false,
  data: null,
  error: error,
});

// 세 가지 액션을 처리하는 리듀서를 만들어줌    
// 기존에 중복되는 값들이 많았던 코드들을 함쳐줌!!    
// type 은 액션 타입, key 는 리듀서에서 사용할 필드 이름
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.js
//UsersContext의 기본 상태
const initialState = {
  users: initialAsyncState,
  user: initialAsyncState,
};

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

// ============= end states ============= //

// 위에서 만든 객체 / 유틸 함수들을 사용하여 리듀서 작성
const 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}`);
  }
};

// State 용 Context와 Dispatch용 Context
const UsersStateContext = createContext(null);
const UsersDispatchContext = createContext(null);

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

/* -> 기존코드
// export const getUsers = async (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 (err) {
//     dispatch({
//       type: "GET_USERS_ERROR",
//       error: err,
//     });
//   }
// };

*/
// 위에서 선언한 두 가지 Context 들의 Provider로 감싸주는 컴포넌트
export const 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 const useUsersState = () => {
  const state = useContext(UsersStateContext);
  if (!state) {
    throw new Error("Cannot find UsersProvider");
  }
  return state;
};

//dispatch 를 쉽게 사용할 수 있게 해주는 hook
export const useUsersDispatch = () => {
  const dispatch = useContext(UsersDispatchContext);
  if (!dispatch) {
    throw new Error("Cannot find UsersProvider");
  }
  return dispatch;
};

'Front-end > React 이론 스터디' 카테고리의 다른 글

Redux Middleware  (0) 2021.06.17
Constate, Recoil  (0) 2021.06.11
CSS Module  (0) 2021.05.31
Context API  (0) 2021.05.29
useCallback  (0) 2021.05.26