oolio
v0.2.9
Published
A universal API client library
Maintainers
Readme
oolio
범용 API 클라이언트 라이브러리입니다. RESTful API를 쉽게 호출할 수 있도록 도와줍니다.
설치
npm install oolio특징
- 라우트 기반의 API 클라이언트
- 자동 인증 토큰 처리 (Bearer)
- 경로 파라미터 지원 (
/user/{userId}) - 본문 자동 직렬화 — 일반 POST/PUT/DELETE는
application/json,payload값에File/Blob, React Native 파일 객체({ uri, type, name? }), Node.jsBuffer가 있으면multipart/form-data로 자동 전환 - 통일된 에러 처리 형식
- 브라우저 / Node.js / React Native 환경 모두 지원
- TypeScript 제네릭으로 요청/응답 타입 정의 가능
fetchOptions로 fetch init 옵션 주입 (credentials,mode,cache,signal등) — 쿠키 인증 지원
사용법
JavaScript
import oolio from "oolio";
const routes = {
auth: {
login: {
method: "post",
path: "/auth/login",
payload: ["email", "password"],
},
},
user: {
getProfile: {
method: "get",
path: "/user/profile",
},
getUserById: {
method: "get",
path: "/user/{userId}",
},
updateUserById: {
method: "put",
path: "/user/{userId}",
payload: ["name", "email"],
},
uploadAvatar: {
method: "post",
path: "/user/avatar",
payload: ["userId", "avatar"],
},
},
};
const api = oolio({
routes,
getAuthorizeToken: () => localStorage.getItem("token"),
baseUrl: "https://api.example.com",
});
// 일반 요청
const response = await api.auth.login({
email: "[email protected]",
password: "1234",
});
// 경로 파라미터
const user = await api.user.getUserById({ userId: "123" });
// 경로 파라미터 + payload
await api.user.updateUserById(
{ userId: "123" },
{ name: "John", email: "[email protected]" },
);
// 파일 업로드
await api.user.uploadAvatar({ userId: "123", avatar: fileInput.files[0] });TypeScript
IO<TPayload, TResponse> 제네릭으로 요청/응답 타입을 정의할 수 있습니다.
import oolio from "oolio";
import type { IO } from "oolio";
const routes = {
auth: {
login: {
method: "post",
path: "/auth/login",
payload: ["email", "password"],
} as IO<{ email: string; password: string }, { token: string }>,
},
user: {
getProfile: {
method: "get",
path: "/user/profile",
} as IO<void, { name: string; avatar: string }>,
getUserById: {
method: "get",
path: "/user/{userId}",
} as IO<void, { id: string; name: string }>,
updateUserById: {
method: "put",
path: "/user/{userId}",
payload: ["name", "email"],
} as IO<{ name: string; email: string }, { success: boolean }>,
uploadAvatar: {
method: "post",
path: "/user/avatar",
payload: ["avatar"],
} as IO<{ avatar: File }, { url: string }>,
},
};
const api = oolio({
routes,
getAuthorizeToken: () => localStorage.getItem("token"),
baseUrl: "https://api.example.com",
});
// 타입 자동 추론
const { token } = await api.auth.login({
email: "[email protected]",
password: "1234",
});
const { name } = await api.user.getProfile();
const { id } = await api.user.getUserById({ userId: "123" });
// 경로 파라미터 + payload
await api.user.updateUserById(
{ userId: "123" },
{ name: "John", email: "[email protected]" },
);
IO타입 규칙:IO<TPayload, TResponse>의TPayload는 body(POST/PUT 등) 또는 query string(GET) 필드 타입만 기술합니다. path의{param}패턴으로 선언한 path params는IO타입에 포함하지 않습니다. path params는 항상Record<string, string>으로 처리되며, 라이브러리가 런타임에 자동으로 분리합니다.// ❌ 잘못된 예: path param을 TPayload에 포함 updateUserById: { method: "put", path: "/user/{userId}", payload: ["name", "email"], } as IO<{ userId: string; name: string; email: string }, { success: boolean }>, // ^^^^^^^^ path param — IO에 넣으면 안 됨 // ✅ 올바른 예: payload 필드(body에 담길 것들)만 기술 updateUserById: { method: "put", path: "/user/{userId}", payload: ["name", "email"], } as IO<{ name: string; email: string }, { success: boolean }>, // ✅ path params만 있고 payload 없는 경우: TPayload는 void getUserById: { method: "get", path: "/user/{userId}", } as IO<void, { id: string; name: string }>,
라우트 옵션
| 옵션 | 필수 | 설명 |
| --------------- | ---- | ---------------------------------------------------------------------- |
| method | O | HTTP 메소드 (get, post, put, delete, patch 등). 대소문자 무관 — 와이어에는 대문자로 정규화되어 전송 |
| path | O | API 엔드포인트 경로. 경로 파라미터는 {param} 형식 |
| payload | - | 요청에 포함될 데이터 필드 목록 (파일 필드도 여기에 함께 명시) |
| authorization | - | false 또는 "guest" 설정 시 토큰 미첨부 (기본값: true) |
| baseUrl | - | 라우트별 baseUrl 오버라이드 |
파일 업로드는 별도 옵션 없이
payload에 키만 명시하면 됩니다. 호출 시 해당 값이File/Blob, React Native 파일 객체({ uri: string, type: string, name?: string }), Node.jsBuffer중 하나이면 자동으로multipart/form-data로 전송됩니다.
메서드 대소문자:
method는 대소문자를 가리지 않습니다. GET 판정("get"/"GET"모두 인식)과 본문 직렬화 분기는 내부적으로 대소문자 무관하게 처리되고, 실제 fetch 호출에 넘기는 와이어 메서드는 대문자로 정규화됩니다("patch"→PATCH).특히 PATCH가 중요합니다 — fetch 스펙은 PATCH를 자동 대문자화 대상에서 제외하므로
fetch(url, { method: "patch" })는 소문자patch가 그대로 와이어에 나가고, 일부 서버(예: Next 16 Node HTTP)는 이를 malformed로 간주해 빈 400으로 끊습니다. oolio는 이를 라이브러리 차원에서 방지하므로 소비측 routes는 메서드를 소문자로 둬도 안전합니다.
클라이언트 옵션 (option)
oolio({ ..., option })에 전달하는 클라이언트 단위 설정.
| 옵션 | 기본값 | 설명 |
| -------------- | ------ | ------------------------------------------------------------------------------ |
| logger | false | true 설정 시 모든 요청·응답·에러를 console에 출력 |
| loggerPretty | false | true 설정 시 객체를 JSON.stringify로 전체 depth 출력 (logger: true일 때만 적용) |
로그 활성화
const api = oolio({
routes,
getAuthorizeToken: () => localStorage.getItem("token"),
baseUrl: "https://api.example.com",
option: { logger: true },
});호출마다 6자 임시 ID가 발급되어 모든 로그 라인 prefix([oolio]:{id})에 포함되므로, 동시 호출 시에도 같은 요청의 로그를 ID로 묶어서 추적할 수 있습니다.
[oolio]:k3p9af → { method: 'post', path: '/auth/login', ... } { pathParams: {}, data: {...}, headers: { Authorization: 'Bearer e...XYZ12345' } }
[oolio]:k3p9af → POST https://api.example.com/auth/login
[oolio]:k3p9af ← POST https://api.example.com/auth/login (245ms) { token: '...' }Authorization 헤더는 토큰 노출을 줄이기 위해 부분 마스킹됩니다(앞/뒤 일부만 표시). 그 외 body·data는 마스킹 없이 그대로 출력되므로 운영 환경에서는 활성화하지 않는 것을 권장합니다.
중첩 객체 전체 출력 (loggerPretty)
기본적으로 Node.js의 console.log는 객체를 depth 2까지만 출력해 중첩된 값이 [Object]로 잘립니다. loggerPretty: true를 함께 설정하면 객체를 JSON.stringify로 포매팅해 전체 내용을 확인할 수 있습니다.
const api = oolio({
routes,
getAuthorizeToken: () => null,
baseUrl: "https://api.example.com",
option: { logger: true, loggerPretty: true },
});// logger: true 만 설정한 경우
[oolio]:k3p9af ← GET https://api.example.com/items (120ms) { data: { items: [Array], total: 1 } }
// loggerPretty: true 추가 시
[oolio]:k3p9af ← GET https://api.example.com/items (120ms) {
"data": {
"items": [
{ "id": 1, "name": "example" }
],
"total": 1
}
}본문 직렬화 규칙
axios의 동작과 유사하게, 메소드와 호출 시 데이터에 따라 자동으로 본문 형식이 결정됩니다.
| 조건 | Content-Type | 본문 |
| --------------------------------------------------------------------------- | ------------------------------------- | ----------------------------- |
| method: "get" (대소문자 무관) | (없음) | URL query string |
| payload 값 중 File/Blob 인스턴스 존재 | multipart/form-data (자동) | FormData (자동 변환) |
| payload 값 중 RN 파일 객체 ({ uri, type, name? }) 존재 | multipart/form-data (자동) | FormData (자동 변환) |
| payload 값 중 Node.js Buffer 존재 | multipart/form-data (자동) | FormData (자동 변환) |
| 호출 시 data로 FormData 인스턴스 직접 전달 | multipart/form-data (자동) | 전달한 FormData 그대로 |
| 그 외 POST/PUT/DELETE | application/json | JSON.stringify(payload) |
- 사용자가
headers["Content-Type"]을 직접 지정한 경우 항상 그 값을 우선합니다 — 자동 분기로 multipart가 되는 경우에도delete하지 않고 사용자가 지정한 값을 그대로 보냅니다 (단,multipart/form-data로 임의 지정 시 boundary는 사용자 책임). payload에 명시되지 않은 키는 자동 감지 대상에서 제외되어 잘려나갑니다 (협업 누락 방지를 위한 의도적 동작).
fetch 옵션 (fetchOptions)
oolio({ ..., fetchOptions })에 전달하면 모든 요청의 fetch(url, init) 호출에 병합되는 init 옵션입니다. credentials, mode, cache, signal, keepalive 등 표준 RequestInit 필드를 지정할 수 있습니다.
쿠키 기반 인증(특히 cross-origin)에는 credentials: "include"가 필요합니다. 지정하지 않으면 fetch 기본값(same-origin)이 적용됩니다.
const api = oolio({
routes,
getAuthorizeToken: () => localStorage.getItem("token"),
baseUrl: "https://api.example.com",
fetchOptions: { credentials: "include" }, // 모든 요청에 쿠키 동봉
});- 호출별 오버라이드: 마지막 인자로
{ fetchOptions }를 전달하면 해당 요청에만 적용됩니다. 클라이언트 레벨fetchOptions와 얕은 병합되며 호출별 값이 우선합니다. - 우선순위: oolio가 관리하는
method/body/headers는 항상 최종 우선합니다.fetchOptions.headers를 지정해도 직렬화 단계에서 만든 헤더(Authorization,Content-Type등)가 같은 키를 덮어씁니다. - 인터셉터 노출: 병합된 값은
RequestConfig.fetchOptions로 들어가request인터셉터에서 읽고 변형할 수 있습니다.
// 이 호출만 same-origin으로 (클라이언트 기본 include를 덮어씀)
await api.user.getProfile({ fetchOptions: { credentials: "same-origin" } });
// pathParams + data + fetchOptions
await api.user.updateUserById(
{ userId: "123" },
{ name: "John", email: "[email protected]" },
{ fetchOptions: { cache: "no-store" } },
);
// headers와 함께 사용
await api.user.getProfile({
headers: { "X-Trace-Id": "abc" },
fetchOptions: { credentials: "include" },
});인터셉터
oolio({ ..., interceptors })에 전달하는 훅. 한 번 등록하면 모든 요청에 자동 적용됩니다.
| 훅 | 시점 | 시그니처 |
| ---------------- | ----------------------------- | ------------------------------------------------------------------------------------------ |
| request | fetch 직전 (직렬화 완료 후) | (config: RequestConfig) => RequestConfig \| Promise<RequestConfig> |
| response | 성공 응답 파싱 후 | (data: any, config: RequestConfig) => any \| Promise<any> |
| responseError | 에러 최종 처리 | (error: OolioError, config: RequestConfig) => any \| Promise<any> |
| retry | 에러 발생 시 재시도 여부 결정 | (error: OolioError, config: RequestConfig, attempt: number) => boolean \| Promise<boolean> |
attempt: 지금까지 실패한 횟수. 첫 실패 후 호출 시attempt=1.retry가true를 반환하면 동일 config로 재시도합니다.responseError보다 먼저 실행되며, retry 포기 후responseError로 넘어갑니다.responseError가 값을 반환하면 해당 값이 호출자에게 전달됩니다 (throw 없음). 직접throw하면 호출자까지 전파됩니다.
const api = oolio({
routes,
getAuthorizeToken: () => localStorage.getItem("token"),
baseUrl: "https://api.example.com",
interceptors: {
// 모든 요청에 트레이스 ID 헤더 추가
request: (config) => {
config.headers["X-Trace-Id"] = crypto.randomUUID();
return config;
},
// 응답 unwrap: { result: ... } 구조라면 result만 반환
response: (data) => data.result ?? data,
// 404는 null로 변환, 나머지는 그대로 throw
responseError: async (err) => {
if (err.status === 404) return null;
throw err;
},
// 503 에러 최대 3회 지수 백오프 재시도
retry: async (err, _config, attempt) => {
if (err.status !== 503 || attempt >= 3) return false;
await new Promise((r) => setTimeout(r, 2 ** attempt * 100));
return true;
},
},
});per-request 옵션
각 API 호출의 마지막 인자로 { headers?: Record<string, string>; fetchOptions?: RequestInit } 오브젝트를 전달하면 해당 요청에만 적용됩니다. 글로벌 인터셉터로 처리하기 어려운 요청별 헤더나 fetch 옵션에 사용합니다.
// path params 없는 route — data 없이 headers만
api.user.getProfile({ headers: { "X-Request-Source": "mobile" } });
// path params 없는 route — data + headers
api.auth.login({ email: "[email protected]", password: "1234" }, { headers: { "X-Trace-Id": "abc" } });
// path params 있는 route — data 생략, pathParams + headers
api.user.getUserById({ userId: "123" }, { headers: { "X-Request-Source": "admin" } });
// path params 있는 route — pathParams + data + headers
api.user.updateUserById(
{ userId: "123" },
{ name: "John", email: "[email protected]" },
{ headers: { "X-Trace-Id": "abc" } },
);headers 또는 fetchOptions 키를 가진 오브젝트를 마지막 인자로 넣으면 options로 인식합니다. data를 생략하고 싶다면 null 없이 바로 붙이면 됩니다.
// path params 있는 route — data 생략
// { headers } 키가 있으므로 options로 자동 인식
api.user.getUserById({ userId: "123" }, { headers: { "X-Custom": "test" } });
// fetchOptions만 단독으로 넘겨도 options로 인식
api.user.getProfile({ fetchOptions: { credentials: "include" } });fetch 옵션 주입에 대한 자세한 내용은 fetch 옵션 (
fetchOptions) 섹션을 참고하세요.
에러 처리
try {
const response = await api.auth.login({
email: "[email protected]",
password: "1234",
});
} catch (error) {
console.error(error.status); // HTTP 상태 코드
console.error(error.statusText); // 상태 텍스트
console.error(error.data); // 서버 응답 데이터
}변경 이력
0.2.9
버그 수정
- 와이어 HTTP 메서드를 대문자로 정규화 — fetch 스펙이 PATCH를 자동 대문자화 대상에서 제외해 소문자
patch가 그대로 전송되던 문제 수정. 일부 서버(예: Next 16 Node HTTP)가 소문자 메서드를 malformed로 간주해 빈 400으로 끊던 함정을 라이브러리 차원에서 방지 - GET 판정 및 본문 직렬화 분기를 대소문자 무관하게 변경 —
method를"GET"(대문자)로 선언해도 정상적으로 query string 직렬화
0.2.8
신규 기능
OolioConfig.fetchOptions추가 — 모든 요청의fetch호출에 병합할 init 옵션(credentials,mode,cache,signal등) 주입. 쿠키 기반 인증(특히 cross-origin)을 위한credentials: "include"지원- per-request
options.fetchOptions추가 — 호출별로 fetch 옵션 오버라이드 (클라이언트 레벨과 얕은 병합, 호출별 우선) - 마지막 인자에
fetchOptions키만 있어도 per-request options로 자동 인식 (기존headers키와 동일하게 동작) - 병합된 fetch 옵션은
RequestConfig.fetchOptions로 노출되어request인터셉터에서 변형 가능. oolio가 관리하는method/body/headers는 항상 최종 우선
0.2.7
버그 수정
- React Native 환경에서 파일 업로드가 동작하지 않던 문제 수정 — RN 파일 객체(
{ uri, type, name? })를 binary로 감지해multipart/form-data로 자동 전환 - Node.js
Buffer를 파일로 업로드할 수 없던 문제 수정 —Buffer인스턴스를 binary로 감지해multipart/form-data로 자동 전환
0.2.6
문서 개선
IO<TPayload, TResponse>타입 규칙 명시 —TPayload는 body/query 필드 전용이며 path params는 포함하지 않는다는 설명과 올바른/잘못된 예시 추가
0.2.5
버그 수정
ApiClient타입이 path params 있는 route에서 인자 2개를 받지 못하던 문제 수정 —(pathParams, data?)시그니처를 오버로드로 추가해 TypeScript 에러 해소
0.2.4
신규 기능
OolioConfig.option.loggerPretty추가 —logger: true일 때 중첩 객체를JSON.stringify로 전체 depth 출력
0.2.3
Breaking changes
IO옵션에서files필드 제거. 대신payload값 중File/Blob인스턴스가 있으면 자동으로multipart/form-data로 전환됩니다. 기존에files를 사용하던 경우 해당 키를payload로 옮기기만 하면 됩니다.- POST/PUT/DELETE 기본 직렬화 방식이
multipart/form-data→application/json으로 변경되었습니다.
신규 기능
OolioConfig.interceptors추가 —request/response/responseError/retry4종 지원OolioConfig.option.logger추가 — 요청·응답·에러 콘솔 출력, 동시 요청 구분용 임시 ID 포함- per-request 옵션 — 마지막 인자로
{ headers }오브젝트 전달 시 해당 요청에만 헤더 적용 - 사용자가
headers["Content-Type"]을 직접 지정한 경우 자동 감지보다 우선 적용 payload값에File/Blob이 있으면 별도 설정 없이multipart/form-data로 자동 전환
라이센스
MIT
