커스텀훅, useAxios 추상화 및 구현기

윤뿔소·2023년 2월 7일
2

팀 프로젝트: 맛피

목록 보기
4/8
post-thumbnail

프로젝트를 진행할 때 맛플레이스, 맛픽커 관련 마커 및 맛포스트 등의 데이터를 fetch 해오는 코드가 많을 것이라고 생각했고, 커스텀훅을 만들어 관리하기 용이하도록 분리해야한다고 논의가 이루어졌다.

서버에서 받아온 데이터를 프론트에서 관리하면서 결과적으로,

  1. 비동기 처리 코드는 10줄 이상이다.
  2. 네트워크 통신을 위한 비동기 처리 코드가 여러번 쓰일 것이다.
  3. 네트워크 통신 코드, Axios가 쓰인 코드를 한 곳에 모아 관리하고 싶다.
  4. Ajax한 결과물에 타입을 설정해야한다. 그러므로 한 곳에 관리하면서 써내려 가고 싶다.

이러한 상태를 만들기 위해 이번 글에서 그 여정을 써본다.

초기 상태

import { useState, useEffect, useCallback } from "react";
import axios from "axios";

interface MemberData {
  nickname: string;
  email: string;
  birthday: string;
  profileImg: string;
  gender: string;
  memo: string;
  createdAt: string;
  modifiedAt: string;
  followers: string;
  followings: string;
  postlist: Array<Post>;
  picklist: Array<Pick>;
}
interface Post {
  postId: number;
  likes: number;
  commentcount: number;
  thumbnail_url: string;
}

interface Pick {
  groupId: number;
  name: string;
  color: string;
}
interface UseAxiosReturn {
  memberData: MemberData | null;
  loading: boolean;
  error: Error | null;
}

const useAxios = (url: string): UseAxiosReturn => {
  const [memberData, setMemberData] = useState<MemberData | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const axiosData = useCallback(async () => {
    setLoading(true);
    try {
      const response = await axios.get<MemberData>(url);
      setMemberData(response.data);
    } catch (error) {
      setError(Object.assign(new Error(), error));
    } finally {
      setLoading(false);
    }
  }, [url]);

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

  return { memberData, loading, error };
};

export default useAxios;

전에 사용했던 커스텀훅을 가져와 이렇게 썼는데 문제가 생겼다.

문제

  1. 커스텀훅의 오용/남용 => 커스텀훅의 기능을 제대로 사용하기 위해 비동기 코드 / 네트워크 통신 코드(Axios) 분리 및 코드 따로 보관
  2. 네트워크 통신 관련 Axios 코드가 컴포넌트 이곳 저곳에 흩어져있음 => 줄이기 위해 커스텀훅과 비슷하게 한 폴더에 관리
  3. 커스텀훅에 대한 이해가 부족해 남용하는 문제가 생김. 추상화를 통해서 저 useAxios하나로 비동기 코드를 관리할 수 있어야 하는데 위 사진처럼 난잡하게 네트워크 통신 코드 및 커스텀훅 코드를 써버렸음.

멘토의 조언

우리 커스텀훅을 보시고 이러한 fetch 훅을 많이 만들어 놨으니 참고하여 프로젝트에 맞게 만들어보라고 말씀하셨다. 되게 도움이 많이 됐다.

useAsync와 같은 추상화 방식이 사용되는 이유는 코드를 보면 아시겠지만 data fetching 시, 중복되는 반복 로직(status, fetching, etc...)을 한 데 모아 추상화 할 수 있다는 장점 때문입니다.
다만, hooks 내부에 data 상태를 api 호출을 통해서만 변경할 수 있기 때문에 멘토링때 다뤘던 MatListView와 같은 컴포넌트에서는 별도의 POST/PUT/DELETE api 호출 후에 다시 GET를 한번 호출해줘야 하는 상황이 생깁니다. 당연히 효율성만 봤을때는 최대한 api 호출을 적게 하고, 프론트에서 처리해주는 게 맞지만, 이 부분은 상황에 따라 POST/PUT/DELETE 후, 다시 GET 해도 큰 문제는 없을 것 같습니다. 보통 그렇게 많이 하기도 합니다.
그렇게 하지 않으려면 하나의 hooks에 GET/POST/PUT/DELETE/ 관련 함수를 전부 넣어 관리해야합니다. useUser라고 예를 들면 return {users, deleteUser, updateUser} 이런 식일텐데, 이 방식도 나쁘진 않지만 useAsync 내부 코드와 같은 중복 로직이 많이 생길 수 있습니다. (노가다가 많이 생깁니다.)
react 진영에 hooks가 생겨나면서 api(비동기)를 통해 가져온 상태 관리(server side state)에 대한 다양한 의견들이 많이 생겨났는데 그중 react-query, swr과 같은 data-fetching 전용 hooks 라이브러리들이 이런 것들을 쉽게 구현할 수 있게 여러 기능들을 제공해주고 있습니다. 인기도 많습니다. 편하기도 합니다. 이건 나중에 회사 가셔서 한번 다룰 기회가 있으면 좋을 것 같네요~
엔지니어링에 은탄환은 없으니 장/단점을 잘 살피셔서 개발에 가장 효율적인 방법을 가져가시면 될 것 같습니다. 기능/구조는 여러분들이 더 잘 아시니까요~
제가 useAsync를 말씀드린 이유는 react-hooks도 추상화를 시켜 사용한다는 점을 말씀드리고 싶었고,
아래 링크 보시면 여러 기본적인 hooks들에 대한 예제가 있는데, 이런 식으로 감싸서 많이 사용하는구나 정도로만 보시고, 추후에 필요하실 때 찾아서 클론 코딩해도 큰 도움이 될 것 같네요~~
https://usehooks.com/
https://swr.vercel.app/
https://react-query-v3.tanstack.com/

요약

커스텀훅으로 추상화을 활용한 useAsync 류는 데이터 가져오기와 같은 중복되는 반복 로직을 모아 추상화할 수 있어 코드의 가독성을 높일 수 있다. 그러나 useAsync 내부 코드는 GET/POST/PUT/DELETE 함수를 모두 포함하지 않으므로 이러한 함수를 사용하려면 별도의 hooks를 만들어야 한다. 이러한 상황에서 react-query, swr과 같은 data-fetching 전용 hooks 라이브러리를 사용하면 많은 기능을 제공해주기 때문에 효율적으로 상태를 관리할 수 있다. 최적의 방법은 상황에 따라 다르기 때문에 장단점을 고려하여 개발을 해야한다. 이와 관련하여 usehooks.com에는 여러 가지 기본적인 hooks들에 대한 예제가 있으므로 필요할 때 참고할 수 있다.

위의 얘기를 듣고 우리는 회의에 들어갔다. 일단 펫칭 커스텀 훅을 참고하여 우리가 관련 커스텀훅을 만들기로 했다. react-query등을 사용해 시간을 절약할 수 있었지만 이걸 직접 만듦으로써 비동기 관련 코드에 대해 더욱 잘 이해할 수 있기 때문에 실패를 거듭하더라도 만들기로 했다.

해결

1. 네트워크 통신 코드와 커스텀훅 코드의 분리

hooks와 api로 나눠 url 관련된 네트워크 통신 코드는 api에, 추상화가 잘 된 커스텀훅은 hooks로 나눠 관리했다.
덕분에 개발서버의 url 바뀔 때 마다 url하나만 바꿔줘도 되고, api 관련 수정사항은 저 폴더 안에서만 해결할 수 있으니 관리/보수가 편해졌다. 좋아!

2. useAxios의 전면적인 개편

import { useState, useEffect, useCallback } from "react";

type Status = "Idle" | "Loading" | "Success" | "Error";
interface UseAxiosReturn<T> {
  axiosData: () => void;
  responseData: T | null;
  status: Status;
}

const useAxios = <T>(
  callback: () => Promise<T>,
  deps: any[] = [],
  skip = false
): UseAxiosReturn<T> => {
  const [responseData, setResponseData] = useState<T | null>(null);
  const [status, setStatus] = useState<Status>("Idle");

  const axiosData = useCallback(async () => {
    setStatus("Loading");
    try {
      const data = await callback();
      setResponseData(data);
      setStatus("Success");
      return data;
    } catch (error) {
      setStatus("Error");
      throw error;
    }
  }, deps);

  useEffect(() => {
    if (skip) return;
    axiosData();
  }, deps);

  return { axiosData, responseData, status };
};

export default useAxios;
  1. 타입
    구체적인 타입 설정은 api 폴더에 있는 네트워크 통신에 넘겼다. 덕분에 각 api에 맞는 타입을 설정하고, 쉬운 관리/보수를 추구할 수 있게 됐다.
  2. status 통일
    loading, error를 하나의 변수로 통일해 관리했다. 덕분에 코드 가독성도 좋아졌다.
  3. ⭐️파라미터 개편
    • 콜백함수(네트워크 통신 함수)
    • deps(의존성 배열 관련 변수): deps는 호출 관련 변수가 변하면 실시간 반영을 위해 받아주는 파라미터
    • skip(페이지 렌더링 후 실행 유무 변수):skiptrue면 페이지 렌더링 됐을 때 말고 나중에 데이터 호출을 실행하기 위해 넣어줄 수 있는 옵션.
  4. return 값 변경
    • axiosData를 리턴해 함수 호출을 원할 때 호출할 수 있게 했다. skiptrue면 무조건 있어야하는 값.
    • responseData: 응답 데이터!
    • status: 불러와졌는지 확인할 수 있는 상태 변수, 조건문에 조건으로 쓸 수 있게 리턴했다!

결론

초기 상태로 이미 남용하여 거의 모든 컴포넌트 및 페이지에 초기 코드를 쓴 상태에서 멘토님에게 조언을 들어 바꿨었다.
이미 쓴 코드를 수정하려고 하니까 진짜 힘들었다. 레퍼런스를 봐도 어떻게 우리가 이걸 최적화할건지와 다 쓴 코드들을 수정하고 수정한 코드에 맞춰서 다시 데이터를 가공하는 것, 이러한 점들이 되게 힘들었다. 설날 때 시골에서 맥북 키고 했었다..

알게된 것은 '역시 바퀴를 다시 발명하는 건 힘들다.'였고 비동기는 생각보다 더 까다롭다. 특히 SPA 특성상 그냥 axios 호출하여 GET하고 PATCH, POST 했을 때 데이터가 오면 실시간 반영이 안됐다. 지금보니 초기 상태의 코드로는 어림도 없었지.. 어떤 변수가 변했고, 그 변수를 다시 get해오는 것을 1부터 10까지 다 해줬어야 했는데 관련 코드가 없었으니까! 으이구!!
또, 아는 만큼 보인다고 useQuery 존재라든지 펫칭 관련 커스텀훅을 모르니 뭐가 문제인지도 몰랐다. 되게 부끄러웠다. 좀더 공부해서 프론트 개발 기획을 잡을 때부터 올바른 길을 걷기 위한 시야를 넓히고 싶었다.

이러한 일들을 겪고나니 다음 번엔 useQuery를 써보고 싶어졌다. 지금 보니 진짜 만능같다. 나중에 후술할 실시간 반영 관련 문제, 무한스크롤 등에 다 쓰이는 것 같았다. 개인프로젝트에 꼭 배운 점들을 반영하여 성장하자.

profile
코뿔소처럼 저돌적으로

6개의 댓글

comment-user-thumbnail
2023년 2월 17일

useQuery 포스팅이 너무 기다려지네요..! 언제 업로드 예정이신가요. 현기증나요.

답글 달기
comment-user-thumbnail
2023년 2월 17일

리팩토링이 진짜 쉬운게 아닌데 너무 멋집니다 리팩토링 더 해주세요..👍

답글 달기
comment-user-thumbnail
2023년 2월 18일

저 어마어마한 양을 리팩토링 하셨다니... 정말 존경합니다

답글 달기
comment-user-thumbnail
2023년 2월 18일

타스 리펙토링 ... 최고에요 ...

답글 달기
comment-user-thumbnail
2023년 2월 18일

useQuery 얼른 보고 싶습니다 저도.. 문제와 해결이 한번에 들어와서 궁금증이 단번에 해결되네요. 리팩토링하신 부분 존경합니다 👍🏻

답글 달기
comment-user-thumbnail
2023년 2월 19일

커스텀 훅에 인자로 콜백함수를 넘겨서 활용할 수도 있다는 걸 배웠습니다..!! 👍

답글 달기