비동기 호출

MountionRiver·2025년 8월 14일

API 요청

fetch로 API 요청하기

  • api 요청 형식
import React, { useEffect, useState } from "react";

const CartBadge: React.FC = () => {
  const [cartCount, setCartCount] = useState(0);

  useEffect(() => {
    fetch("https://api.baemin.com/cart")
      .then((response) => response.json())
      .then(({ cartItem }) => {
        setCartCount(cartItem.length);
      });
  }, []);

  return {/* cartCount 상태를 이용하여 컴포넌트 렌더링 */}</>;
};

모든 api에서 동일한 추가 기능(커스텀 헤더, 타임아웃 등)을 원할경우 모든 api 요청 함수를 찾고, 하나하나 수정해야함. -> 비효율적

서비스 레이어로 분리하기

  • api 수정이 필요할 경우가 많기 때문에 컴포넌트로 분리해는것이 더 낫다.
  • 함수 호출을 서비스 레이어로 이동하고, 컴포넌트에서 api 요청이 필요할 경우 서비스레이어에 있는 비동기 함수를 호출하는 방식이 좋다.
  • 레이어로 분리해도 동일한 요구사항(커스텀 헤더, 타임아웃 등 )이 추가 될 경우 모든 api 에 넣어야 하는것은 동일 -> 비효율적
// 이 코드 또한 변경사항을 하나하나 넣어야함으로 비효율적이다.
async function fetchCart() {
  const controller = new AbortController();

  const timeoutId = setTimeout(() => controller.abort(), 5000);

  const response = await fetch("https://api.baemin.com/cart", {
    signal: controller.signal,
  });

  clearTimeout(timeoutId);

  return response;
}

Axios 활용하기

  • fetch 는 기본 내장 라이브러리라 추가 기능 하려면 직접구현해야함 -> 그거 귀찮아서 Axios 쓴다.

  • api entry 가 여러개면 instance 여러개 만들면 됨

Axios 인터셉터 활용하기

  • 인터셉터를 사용해서 비동기 호출 내용 추가 할 수도 있고, 에러 묶어서 처리 가능함. 아래는 예시코드
// 각가 다른 헤더를 넣는 인터셉트
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";

const getUserToken = () => "";
const getAgent = () => "";
const getOrderClientToken = () => "";
const orderApiBaseUrl = "";
const orderCartApiBaseUrl = "";
const defaultConfig = {};
const httpErrorHandler = () => {};

const apiRequester: AxiosInstance = axios.create({
  baseURL: "https://api.baemin.com",
  timeout: 5000,
});

const setRequestDefaultHeader = (requestConfig: AxiosRequestConfig) => {
  const config = requestConfig;
  config.headers = {
    ...config.headers,
    "Content-Type": "application/json;charset=utf-8",
    user: getUserToken(),
    agent: getAgent(),
  };
  return config;
};

const setOrderRequestDefaultHeader = (requestConfig: AxiosRequestConfig) => {
  const config = requestConfig;
  config.headers = {
    ...config.headers,
    "Content-Type": "application/json;charset=utf-8",
    "order-client": getOrderClientToken(),
  };
  return config;
};

// `interceptors` 기능을 사용해 header를 설정하는 기능을 넣거나 에러를 처리할 수 있다
apiRequester.interceptors.request.use(setRequestDefaultHeader);
const orderApiRequester: AxiosInstance = axios.create({
  baseURL: orderApiBaseUrl,
  ...defaultConfig,
});
// 기본 apiRequester와는 다른 header를 설정하는 `interceptors`
orderApiRequester.interceptors.request.use(setOrderRequestDefaultHeader);
// `interceptors`를 사용해 httpError 같은 API 에러를 처리할 수도 있다
orderApiRequester.interceptors.response.use(
  (response: AxiosResponse) => response,
  httpErrorHandler
);
const orderCartApiRequester: AxiosInstance = axios.create({
  baseURL: orderCartApiBaseUrl,
  ...defaultConfig,
});
orderCartApiRequester.interceptors.request.use(setRequestDefaultHeader);

그 아래는 이런식으로 api 호출은 우리는 만들어서 쓴다.

API 응답 타입 지정하기

  • 일단 api 데이터 받는 형식은 얼추 다 비슷하다
interface Response<T> { 
  data: T;
  status: string;
  serverDateTime: string;
  errorCode?: string; // FAIL, ERROR
  errorMessage?: string; // FAIL, ERROR 
}

아래 형식으로 지정해서 그냥 다 묶어 버리면 편하다.
근데 update나 create는 데이터를 받는게 아니라 보내는 거니깐 굳이 데이터를 받을 필요가 없다.
근데 위 형식으로 묶어버리면 불필요한 데이터를 받아야 한다는거지? 그게 싫다.
그러니 세부 데이터 타입 설정은 쓰는 데서 해라.

  • 데이터는 프론트에서 굳이 안건드리는 것도 있다. 그냥 서버에서 서버로 넘겨주는것들. 이런것도 굳이 타입을지정해줄 필요가 없다. 하지만 써야 될 수도 있잖아? 그러니깐 그냥 unknown 으로 넣어버려. 나중에 아래서 타입 설정 해주면 됨 ㅅㄱ.
interface response {
  data: {
    cartItems: CartItem[];
    forPass: unknown;
  };
}

// 필요하면 아래처럼
type ForPass = {
  type: "A" | "B" | "C";
};

const isTargetValue = () => (data.forPass as ForPass).type === "A";

View Model 사용하기

  • 데이터 받는 방식 다 적어놨는데 그거 나중에 바뀌면 어캄? 뭐 어떻게 해 다 수정해야지. 근데 그거 여러군데서 씀 ㅅㄱ 다바꿔야함.
    그래서 그대로 쓰는게 아니라 이름바꿔서 쓰는거임
    예를들어서 items라는걸 백엔드서 받는데 이게 lists로 바뀌면 어캄?
    뭐 items.ㅇㅇㅇ 이렇게 있는거 뭐 다 list.ㅇㅇㅇ 으로 바꿔야지 뭐.

장점
근데 test = items 로 하고 test.ㅇㅇㅇ으로 쓰고 있다면
test = items를 test = lists 로만 하면 된다는거임.
프론트에서만 추가하고 싶은거있으면 그냥 뷰모델 부분에 추가하고.

단점
근데 인놈 여러군데서 쓰면 코드가 복잡해지고, 관리 개발도 쉽지 않음.... 그래서 일단 뷰모델 적게 쓰고, 백엔드랑 잘~ 이야기해서 변경점 최대한 적게, 그리고 뷰모델쓰면 어디서 끌어왔는지 달아주기 ㄱㄱ

Suptrestruct를 사용해 런타임에서 응답 타입 검증하기

너거 Superstruct 이거 써봄? 이거 런타임에 데이터 타입 검증해줌 어케쓰는지 함 볼래? 아래는 예시 코드임

import {
  assert,
  is,
  validate,
  object,
  number,
  string,
  array,
} from "superstruct";

const Article = object({
  id: number(),
  title: string(),
  tags: array(string()),
  author: object({
    id: number(),
  }),
});

const data = {
  id: 34,
  title: "Hello World",
  tags: ["news", "features"],
  author: {
    id: 1,
  },
};

assert(data, Article);
is(data, Article);
validate(data, Article);

야 이거 함 봐라

const Article = object({ }) 요 안에 받는 데이터형식 넣어주고,
assert는 아니다 싶으면 에러를 던져.
is는 통과했는지 boolean 값을 반환하고,
validate는 [error, data] 형식의 튜플을 반환한다. 유효하지 않을 때는 에러 값이 반환되고, 유효한 경우에는 첫 번째 요소로 undefined, 두 번째 요소로 data value가 반환된다.

자 그럼 이거 타입스크립트랑 어케쓰는데? 어케쓰긴 data 값에다가 내가 원하는 값의 타입 박아넣고 매칭되는지 확인하면 되지롱
아니면 아니라도 에러뜰꺼아니아 데이터 이런식으로 오는데, 느가 적은건 이래. 정적말고 동적으로도 확인 가능하다~ 이거야

실제 API 응답 시의 Suptrestruct 활용사례

interface ListItem {
  id: string;
  content: string;
}

interface ListResponse {
  items: ListItem[];
}
const fetchList = async (filter?: ListFetchFilter): Promise<ListResponse> => {
  const { data } = await api
    .params({ ...filter })
    .get("/apis/get-list-summaries")
    .call<Response<ListResponse>>();

  return { data };
};


import { assert } from "superstruct";

function isListItem(listItems: ListItem[]) {
  listItems.forEach((listItem) => assert(listItem, ListItem));
}

API 상태 관리하기

보통 api는 컴포넌트에서 안부름. 훅이나 라이브러리에서 가공해서 씀

상태 관리 라이브러리에서 호출하기

상태 관리 라이브러리의 비동기 함수들은 비동기 상태를 변화 시키는 코드를 가지고 있음. 컴포넌트는 비동기 함수가 아닌 상태관리 라이브러리의 바뀐 상태를 가지고 와서 렌더링한다.

스토어에서 api를 호출하고 그 결과를 받아와서 상태를 변환 시키는 것이 일반적이나, 호출할때 , 호출한 뒤 , 에러가 발생했을시 상태를 업데이트 해야한다.

Redux — 보일러플레이트(중복 코드)가 많음

  1. useSelector로 현재 상태 읽기.
  2. useDispatch로 액션을 날려 상태 변경.
  3. API 호출 → 응답 받으면 다시 액션으로 상태 업데이트.
  4. API 호출 전/성공/실패 시마다 setApiCall 같은 액션을 여러 번 호출.
  • Redux는 비동기 전용이 아니라 전역 상태 관리가 주목적 → 비동기 처리를 위해 미들웨어 사용.
  • API 요청 상태(로딩, 성공, 실패)를 관리하는 코드가 반복됨.
  • 예: 요청할 때 status: Request, 성공하면 status: None, 실패해도 status: None으로 변경하는 코드를 매번 작성. => 보일러 플레이트 많음

MobX 방식 — 비동기 상태 관리가 간단

  1. makeAutoObservable로 스토어 생성 → 상태와 메서드 자동 반응형 처리.
  2. API 호출 전에 state = "PENDING", 성공하면 "DONE", 실패하면 "ERROR".
  3. MobX의 runInAction을 써서 비동기 콜백 내부에서 안전하게 상태 변경.
  • 비동기 로직과 상태 변경 로직을 한 함수에 묶을 수 있음.
  • async/awaitflow를 활용해 비동기 상태를 직관적으로 작성 가능.
  • 코드량이 Redux보다 적고 간결.

위의 전역상태 문제점

  • 액션이 늘어날수록 스토어나 상태 필드가 계속 증가.
  • 전역 상태 관리자는 모든 비동기 상태에 접근 -> 여러 컴포넌트에서 같은 비동기상태 쓸 시 의도치 않은 곳에서 상태 변경 가능성 높아짐

Zustand — 함수형 스토어

  1. create()로 스토어 생성 → 상태/액션을 한 곳에 정의.
  2. 액션 안에서 API 호출 후 set()으로 상태 갱신.
  3. useStore(selector)로 필요한 조각만 구독 → 불필요 리렌더 줄이기.
  • 액션에 비동기 로직을 곧바로 작성 가능(미들웨어 필수 아님).
  • 보일러플레이트 매우 적음. (reducers/types/action creators 없음)
  • 미들웨어로 확장 쉬움: persist, immer, devtools, subscribeWithSelector.
  • 로딩/에러 플래그를 직접 관리(요청 전/성공/실패 상태 매번 세팅).
  • 전역 스토어이기 때문에 설계 안 하면 의도치 않은 공유/덮어쓰기 발생 가능.
  • 서버 상태(캐싱/중복요청 방지/동기화)는 직접 구현해야 함 → 보통 TanStack Query와 병행.
// store.ts (Zustand)
import { create } from 'zustand';

type Item = { id: number; name: string };
type State = {
  items: Item[];
  loading: boolean;
  error?: string;
  fetchItems: () => Promise<void>;
};

export const useItemStore = create<State>((set, get) => ({
  items: [],
  loading: false,
  async fetchItems() {
    set({ loading: true, error: undefined });
    try {
      const res = await fetch('/api/items').then(r => r.json());
      set({ items: res.items, loading: false });
    } catch (e: any) {
      set({ loading: false, error: e?.message ?? 'error' });
    }
  },
}));

// 컴포넌트
const items = useItemStore(s => s.items);
const loading = useItemStore(s => s.loading);

Jotai — Atom 단위로 쪼갬

  1. 상태를 atom으로 쪼갬. 각 atom은 독립적으로 구독/갱신.
  2. 비동기는 비동기 atom 혹은 loadable 유틸로 관리(필요시 Suspense 연계).
  3. 의존성이 있는 상태는 atom 간 참조로 선언적으로 연결.
  • 아주 미세한 단위로 구독 → 필요한 컴포넌트만 리렌더.
  • 비동기와 의존관계를 선언적으로 표현(비동기 atom, loadable, atomFamily).
  • 상태가 atom으로 흩어지기 쉬움 → 설계/조직화 안 하면 추적 어려움.
  • 전역으로 남발하면 의도치 않은 공유 발생
// atoms.ts (Jotai)
import { atom } from 'jotai';
import { loadable } from 'jotai/utils';

type Item = { id: number; name: string };

const itemsAtom = atom(async () => {
  const res = await fetch('/api/items').then(r => r.json());
  return res.items as Item[];
});

// 로딩/에러 상태를 명시적으로 다루고 싶다면:
export const itemsLoadableAtom = loadable(itemsAtom);

// 컴포넌트
const loadableItems = useAtomValue(itemsLoadableAtom);
if (loadableItems.state === 'loading') return <Spinner/>;
if (loadableItems.state === 'hasError') return <Error msg={String(loadableItems.error)}/>;
return <List data={loadableItems.data}/>;

훅으로 호출하기

리액트 쿼리나 useSwr 훅은 캐시를 사용해서 비동기 함수를 호출함 의도치 않은 상태 변경 방지가 도움됨

// Job 목록을 불러오는 훅
const useFetchJobList = () => {
  return useQuery(["fetchJobList"], async () => {
    const response = await JobService.fetchJobList(); // View Model을 사용해서 결과
    return new JobList(response);
  });
};
  • useQuery: 데이터 조회용 훅
  • 첫 번째 인자 "fetchJobList" → 이 요청의 캐시 식별자(key)
  • 두 번째 인자 → 실제 데이터 fetch 로직
  • JobService.fetchJobList() → API 호출
  • new JobList(response) → ViewModel로 변환해 UI가 쓰기 좋은 형태로 리턴
  • 반환값: isLoading, isError, data, refetch 등 상태 + 데이터
// Job 1개를 업데이트하는 훅
const useUpdateJob = (
  id: number,
  { onSuccess, ...options }: UseMutationOptions<void, Error, JobUpdateFormValue>
) => {
  const queryClient = useQueryClient();

  return useMutation(
    ["updateJob", id], // mutation key
    async (jobUpdateForm: JobUpdateFormValue) => {
      await JobService.updateJob(id, jobUpdateForm); // API 호출 (PUT/PATCH)
    },
    {
      onSuccess: (
        data: void, // updateJob의 return 값은 없다 (status 200으로만 성공 판별)
        values: JobUpdateFormValue,
        context: unknown
      ) => {
        // 'fetchJobList' 캐시 무효화 → 다음 조회 시 최신 데이터로 갱신
        // 성공 시 ‘fetchJobList’를 유효하지 않음으로 설정 
        queryClient.invalidateQueries(["fetchJobList"]);
        // 옵션으로 받은 onSuccess 콜백 실행
        onSuccess && onSuccess(data, values, context);
      },
      ...options,
    }
  );
};
  • useMutation: 데이터 수정/삭제/생성용 훅
  • API 호출 성공 시 fetchJobList 쿼리의 캐시를 invalidate 해서 다음 조회 때 새 데이터 받도록 함
  • onSuccess 콜백을 외부에서 받아 실행 가능
const JobList: React.FC = () => {
  // 비동기 데이터를 필요한 컴포넌트에서 자체 상태로 저장
  const {
    isLoading,
    isError,
    error,
    refetch,
    data: jobList,
  } = useFetchJobList();

  // 간단한 Polling 로직, 실시간으로 화면이 갱신돼야 하는 요구가 없어서 // 30초 간격으로 갱신한다
  useInterval(() => refetch(), 30000);

  // Loading인 경우에도 화면에 표시해준다
  if (isLoading) return <LoadingSpinner />;

  // Error에 관한 내용은 11.3 API 에러 핸들링에서 더 자세하게 다룬다
  if (isError) return <ErrorAlert error={error} />;

  return (
    <>
      {jobList.map((job) => (
        <Job job={job} />
      ))}
    </>
  );
};
  • useFetchJobList() 훅을 호출해 잡 목록 데이터와 상태값 가져옴
  • useInterval로 30초마다 refetch() 호출 → polling
  • isLoading이면 로딩 UI 표시, isError이면 에러 UI 표시
  • 정상 데이터면 jobList.map()으로 Job 컴포넌트를 반복 렌더링

비동기는 리액트 쿼리가 좋다.

API 에러 핸들링

타입 가드 활용하기

  • 타입가드 써서 에러 명시적으로 쓸수 있음

에러 서브클래싱하기

  • 에러 종류 많음 그니깐 에러 어떤 에러인지 분리해서 처리 가능

인터셉터를 활용한 에러 처리

  • 인터셉터에서 401일 경우 토큰 재발급등

에러 바운더리를 활용한 에러 처리

  • 에러 발생시 에러 페이지로 던짐 여기서

상태 관리 라이브러리에서의 에러 처리

  • 상태 관리 라이브러리에서 에러처리 로딩 처리 등 할 수 있음

react-query를 활용한 에러 처리

  • 리액트쿼리에서는 애초에 해야함 로딩처리 에러처리 등등

그 밖의 에러 처리

  • 커스텀해서 만들수있음 함 만들어봏셈

API 모킹

JSON 파일 불러오기

NextApiHandler 활용하기

API요청 핸들러에 분기 추가하기

axios-mock-adapter로 모킹하기

목업 사용 여부 제어하기

0개의 댓글