카테고리 없음

HTTP 클라이언트 라이브러리 ky ㅡ 키 크다~라는 말은 기분이 나쁘다! 나 (번들 사이즈)작거든!

taek2-0310 2026. 5. 8. 16:27

ky란? 🔍

ky는 브라우저 내장 fetch API를 기반으로 만든 경량 HTTP 클라이언트입니다. sindresorhus가 만들었고 순수 ESM 패키지로 배포됩니다.

pnpm install ky
import ky from 'ky';

const data = await ky.get('https://api.example.com/users').json();

딱 이게 전부입니다. fetch를 직접 쓸 때처럼 response.json()을 따로 호출하거나 Content-Type 헤더를 매번 설정할 필요가 없어요. 

 

axios 대신 ky를 써야 하는 이유⭐️

1. 성능차이에 비해 번들 사이즈가 압도적으로 작다.

  • 단일 GET 요청: Axios가 약 3% 빠름
  • POST 요청: Ky가 약 3% 빠름
  • 동시 요청: Axios가 약 3% 빠름

번들 사이즈에 민감한 프로젝트라면 이 수치가 전부를 말해줍니다.

axios ~13 KB
ky ~4 KB

KY(3KB)와 Axios(13KB)가 번들 차이가 약 4배 차이 발생합니다. axios는 XMLHttpRequest 기반으로 브라우저/Node.js 호환성을 위한 어댑터 레이어가 포함돼 있어서 무겁습니다. ky는 브라우저 네이티브 fetch만 씁니다. 이미 브라우저에 내장된 걸 쓰니까 번들에 실어야 할 게 없어요.

2. fetch 기반 — 폴리필이 필요 없다

axios는 내부적으로 XMLHttpRequest를 씁니다. 최신 브라우저 환경에서는 굳이 XHR을 쓸 이유가 없죠. ky는 fetch를 그대로 활용하기 때문에 추가 폴리필 없이 현대 브라우저에서 바로 동작합니다.

Node.js 18+도 fetch가 내장돼 있어서 서버 사이드에서도 별도 설정 없이 씁니다.

(최근 pwa에 관심이 생겨 찾아보던 중 pwa에 핵심 기능들이 대부분 Service Worker 를 통해 구현된다는 사실을 알게 되었습니다. XHR 기반의 axios를 서비스 워커 내에서 사용하려면 별도의 폴리필이나 특수 구성이 필요하고 이로 인해 번들 크기를 증가시킬 수 있다는 것을 알게 습니다.)

3. 더 직관적인 API

// axios — HTTP 에러(4xx, 5xx)는 catch로 떨어짐
try {
  const res = await axios.get('/api/user');
  console.log(res.data); // 한 번 더 .data를 벗겨야 함
} catch (err) {
  if (axios.isAxiosError(err)) {
    console.log(err.response?.status);
  }
}
// ky — 훨씬 깔끔
try {
  const data = await ky.get('/api/user').json();
  console.log(data); // 바로 데이터
} catch (err) {
  if (err instanceof HTTPError) {
    console.log(err.response.status);
  }
}

ky는 4xx, 5xx 응답을 자동으로 HTTPError로 throw합니다. fetch의 가장 큰 함정인 "에러 응답도 resolve된다"는 문제를 해결해줘요.

4. 내장 retry

const data = await ky.get('/api/data', {
  retry: {
    limit: 3,
    methods: ['get'],
    statusCodes: [408, 500, 502, 503, 504],
  },
}).json();

네트워크 불안정 상황에서 재시도 로직이 필요할 때, axios는 직접 구현하거나 라이브러리를 추가해야 합니다. ky는 옵션 하나로 끝납니다.

5. Tree-shakeable ESM

ky는 순수 ESM 패키지입니다. 번들러가 실제로 사용하는 코드만 포함시킬 수 있어요. axios는 CommonJS 기반이라 이 이점을 활용하기 어렵습니다.

보통 이렇게 사용해요🚪

공통 인스턴스 설정

매 요청마다 baseURL, 헤더를 반복하지 않도록 인스턴스를 만들어 씁니다.

import ky from "ky";

export const api = ky.create({
  prefix: import.meta.env.VITE_BASE_URL,
  retry: 0,
});

인증 토큰 자동 주입 (hooks)

axios의 인터셉터에 해당하는 게 ky의 hooks입니다.

// lib/api.ts
import ky from 'ky';

export const authApi = ky.create({
  prefixUrl: process.env.NEXT_PUBLIC_API_URL,
  hooks: {
    beforeRequest: [
      request => {
        const token = localStorage.getItem('access_token');
        if (token) {
          request.headers.set('Authorization', `Bearer ${token}`);
        }
      },
    ],
  },
});

토큰 만료 시 자동 갱신

액세스 토큰이 만료됐을 때 리프레시 토큰으로 재발급하고 원래 요청을 재시도하는 패턴입니다.

import ky, { HTTPError } from 'ky';

export const authApi = ky.create({
  prefixUrl: process.env.NEXT_PUBLIC_API_URL,
  hooks: {
    beforeRequest: [
      request => {
        const token = localStorage.getItem('access_token');
        if (token) {
          request.headers.set('Authorization', `Bearer ${token}`);
        }
      },
    ],
    afterResponse: [
      async (request, options, response) => {
        if (response.status === 401) {
          // 토큰 갱신 시도
          const refreshToken = localStorage.getItem('refresh_token');
          const refreshed = await ky
            .post('/auth/refresh', { json: { refreshToken } })
            .json<{ accessToken: string }>();

          localStorage.setItem('access_token', refreshed.accessToken);

          // 원래 요청 헤더 업데이트 후 재시도
          request.headers.set('Authorization', `Bearer ${refreshed.accessToken}`);
          return ky(request);
        }
      },
    ],
  },
});

공통 에러 처리

import ky, { HTTPError } from 'ky';

export const api = ky.create({
  prefixUrl: process.env.NEXT_PUBLIC_API_URL,
  hooks: {
    afterResponse: [
      async (_request, _options, response) => {
        if (!response.ok) {
          const error = await response.json<{ message: string }>();
          // 전역 토스트 알림 등
          showToast(error.message ?? '요청에 실패했습니다.');
        }
      },
    ],
  },
});

특정 요청에서만 에러를 직접 처리하고 싶다면 throwHttpErrors: false를 쓰면 됩니다.

TypeScript와 함께 쓰기

.json<T>()에 제네릭을 넘기면 타입이 바로 붙습니다.

interface User {
  id: number;
  name: string;
  email: string;
}

// GET
const user = await api.get('users/1').json<User>();

// POST
const created = await api.post('users', {
  json: { name: '홍길동', email: 'hong@example.com' },
}).json<User>();

// 배열
const users = await api.get('users').json<User[]>();

마치며🍔

ky가 axios보다 무조건 낫다고 말하기는 어렵습니다. axios는 Node.js 구버전 지원, 업로드 진행률 추적 등 ky가 지원하지 않는 기능이 있고, 생태계도 훨씬 넓어요.

하지만 모던 브라우저 환경, 번들 사이즈 최적화, ESM 기반 프로젝트라면 ky는 충분히 설득력 있는 선택입니다. axios를 관성적으로 써왔다면 한 번 전환을 고려해볼 만해요.

참고🍿

https://github.com/team-kareer/kareer-client/pull/21

 

Init(web): Ky 세팅 by huniversal · Pull Request #21 · team-kareer/kareer-client

📌 Summary [Init] Ky 서버 통신 라이브러리 세팅 #16 📚 Tasks Ky 라이브러리 세팅 JSONPlaceholder Test 코드 실행 KY란? 네이티브 Fetch API를 기반으로 하는 가벼운 HTTP 클라이언트 라이브러리 핵심 특징 Fetch

github.com

https://velog.io/@antisdun/axios-vs-ky-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EA%B5%AC%ED%98%84

 

axios vs ky 인스턴스 구현

ky를 사용한 이유 PWA 프로젝트를 진행하면서 서비스 워커에 대해 찾아보던 중, XHR 기반의 axios를 서비스 워커 내에서 사용하려면 별도의 폴리필이나 특수 구성이 필요하고 이로 인해 번들 크기를

velog.io