재밌는 ky 라이브러리

houndhollis·2026년 1월 31일


최근 리액트 과제를 진행하던 중, ky 라이브러리를 한 번 사용해보고자 하여 작업을 진행하게 되었습니다.
생각보다 괜찮은 점이 많아, 이번 기회에 간단하게 정리해보고자 합니다.

ky vs axios 간단 비교

항목kyaxios
기반Fetch APIXMLHttpRequest
번들 사이즈매우 작음상대적으로 큼
HTTP 에러 처리4xx/5xx 자동 throw수동 처리
인터셉트 방식hookinterceptor
SSR / Edge유리설정 필요

ky 보러가기

ky를 선택한 이유

axios는 오랫동안 사실상 표준처럼 사용돼 왔지만, 이번에 사용한 Vite 환경에서는
Fetch 기반의 ky도 잘 어울릴 것 같다고 느꼈습니다.

특히 추후 Next.js 환경에서 번들 사이즈, 웹 표준 API 사용, 명확한 에러 흐름을 중요하게 보게 되면서
이참에 ky를 한 번 제대로 사용해보자는 생각으로 선택하게 되었습니다.

이번 글에서는 실제로 ky를 사용해 공통 API Client를 구성하고, 실제 API 레이어에서 어떻게 사용했는지 정리해보겠습니다.

공통 API Client 구성

API를 호출할 때마다 토큰을 붙이거나, 에러 코드를 분기 처리하는 로직이 여기저기 흩어지면 유지보수가 어려워지기 마련입니다.

그래서 먼저 ky.create를 사용해 모든 요청의 진입점이 되는 base client를 구성했습니다.

import ky from 'ky';
import Cookies from 'js-cookie';
import { FAILURE_CODES, type BaseResponseType } from '../types';
import { removeToken } from '@/shared/utils/auth';

export const baseApiClient = ky.create({
  prefixUrl: '/api',
  hooks: {
    beforeRequest: [
      (request) => {
        const token = Cookies.get('accessToken');

        if (token) {
          request.headers.set('Authorization', `Bearer ${token}`);
        }
      },
    ],
    afterResponse: [
      async (_request, _options, response) => {
        if (response.ok) {
          return;
        }

        const res = (await response.json()) as BaseResponseType<{}>;

        // 인증 만료 시 토큰 제거
        if (res.code === FAILURE_CODES.UNAUTHORIZED) {
          return removeToken();
        }

        throw {
          code: res.code,
          data: res.data,
          message: res.message,
        };
      },
    ],
  },
});

물론 해당 baseApiClient에서는 많은 내용을 다루진 않지만 중요한 포인트로 뽑는다면,

첫 번째는 beforeRequest hook입니다.
요청이 나가기 직전에 실행되기 때문에, 쿠키에 저장된 토큰을 읽어 Authorization 헤더에 자동으로 주입할 수 있습니다. 이 덕분에 이후 API 호출부에서는 인증 관련 코드를 전혀 신경 쓰지 않아도 됩니다.

두 번째는 afterResponse hook입니다.
ky는 기본적으로 4xx, 5xx 응답을 에러로 판단하지만,
여기서는 한 단계 더 나아가 서버에서 내려주는 공통 응답 구조의 code 값을 기준으로 에러를 처리했습니다.
인증이 만료된 경우에는 토큰을 제거하고, 그 외의 경우에는 도메인 에러 객체를 throw하도록 구성했습니다.


HTTP 메서드 래핑

baseApiClient 위에 바로 ky를 사용해도 되지만, 응답 타입과 호출 방식을 통일하기 위해 한 번 더 래핑을 했습니다.

import { baseApiClient } from './baseClient';
import type { BaseResponseType } from '../types';
import type { Options } from 'ky';

export const client = {
  get: async <T>(url: string, options?: Options) =>
    baseApiClient.get(url, options).json<BaseResponseType<T>>(),

  post: async <T>(url: string, options?: Options) =>
    baseApiClient.post(url, options).json<BaseResponseType<T>>(),

  put: async <T>(url: string, options?: Options) =>
    baseApiClient.put(url, options).json<BaseResponseType<T>>(),

  patch: async <T>(url: string, options?: Options) =>
    baseApiClient.patch(url, options).json<BaseResponseType<T>>(),

  delete: async <T>(url: string, options?: Options) =>
    baseApiClient.delete(url, options).json<BaseResponseType<T>>(),
};

이 구조의 목적은 단순합니다.
모든 API 응답은 BaseResponseType<T> 형태로만 받는다는 규칙을 강제하는 것입니다.

API 사용부에서는 HTTP 구현이나 파싱 로직을 전혀 알 필요가 없고,
오직 "어떤 데이터를 요청하고 어떤 타입을 받는지만" 신경 쓰면 됩니다.


실제 사용 예시

import { client } from '../client/apiClient';
import type { AuthLoginResponse } from './types';

// ------------------ 로그인 API -----------------------
export const loginWithEmail = async (email: string) => {
  const res = await client.post<AuthLoginResponse>('auth/login', {
    searchParams: {
      email,
    },
  });

  return res;
};
  • 토큰 처리 없음
  • 에러 분기 없음
  • 응답 파싱 없음

API 함수는 비즈니스 의미만 담은 코드가 되고, 공통 로직은 모두 API Client 레이어에서 처리됩니다.

정리

ky는 axios를 완전히 대체하기 위한 라이브러리라기보다는,
Fetch 기반 환경에서 더 단순하고 가벼운 API 레이어를 구성하기 위한 선택지에 가깝다고 느꼈습니다.

  • 번들 사이즈가 작고
  • hook 기반으로 공통 로직을 관리하기 쉬우며
  • 타입 안정성이 높은 API 구조를 만들 수 있다는 점에서
    충분히 매력적인 선택지라고 생각합니다.

실제 사용된 레포지스토리

여기까지 재밌는 ky 라이브러리에 대해 알아보았습니다.

이상입니다.
이 글이 도움이 되셨다면 다행입니다.

profile
한 줄 소개

2개의 댓글

comment-user-thumbnail
2026년 2월 4일

axios도 슬슬 대체되는 분위기인걸까요..! 새로운 라이브러리를 접하면 신기하기도 하고 알아가기 귀찮은 마음이 동시에 드는 요즘이네요 ㅠㅠㅠ 덕분에 알아갑니다!

답글 달기
comment-user-thumbnail
2026년 2월 24일

이전 직장에서 사수분이 적용해두셨던 구조와 유사하네요. 왜 ky를 사용하는지 알기 쉽게 정리되었습니다. 감사합니다.

답글 달기