폰트 API
폰트 89종의 메타데이터를 JSON 으로 제공합니다. 인증도, 키도 필요 없습니다. CORS 가 열려 있어 브라우저에서 바로 부를 수 있고, IP당 시간당 300회까지 호출됩니다.
엔드포인트
| 메서드 | 경로 | 설명 |
|---|---|---|
GET | /api | 엔드포인트 안내 + 폰트 수 |
GET | /api/fonts | 전체 폰트 목록 (count, fonts[]) |
GET | /api/fonts/{slug} | 폰트 상세 · 없으면 404 {"error":"not_found"} |
응답 예시 GET /api/fonts/noto-sans-kr
{
"slug": "noto-sans-kr",
"name": "Noto Sans KR",
"ko": "노토 산스 KR",
"category": "산세리프",
"license": "OFL 1.1",
"weights": [
400,
700
],
"stack": "'Noto Sans KR', sans-serif",
"copyright": "(c) 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.",
"maker": "Adobe",
"hangul": {
"covered": 11172,
"total": 11172,
"percent": 100
},
"coverage": {
"hanja": 8138,
"kana": 189,
"box": 128,
"jamo": 256,
"latin": 53
},
"webfont": {
"subset": true,
"files": 286,
"bytes": 5697444,
"sample_load_bytes": 125696
},
"metrics": {
"ink": 0.2114,
"width": 0.92
},
"line_height": {
"default": 1.448,
"recommended": 1.45,
"consistent": true,
"spread_em": 0,
"by_env": {
"mac_linux": 1.448,
"windows": 1.448
}
},
"readability": {
"role": "body",
"grade": "A",
"score": 96,
"label": "본문 권장",
"notes": [
"굵기가 2종이라 본문·강조 사이의 중간 계조가 없습니다."
]
},
"css": "https://cdn.textyle.net/f/noto-sans-kr.css",
"url": "https://textyle.net/font/noto-sans-kr",
"likes": 0,
"views": 64
}
필드
| 필드 | 타입 | 설명 |
|---|---|---|
slug | string | 폰트 식별자. CSS·페이지 URL 에 그대로 쓰입니다 |
ko / name | string | 한글 이름 / 원래 패밀리명 |
weights | int[] | 제공하는 굵기 (400, 700 …) |
stack | string | CSS font-family 에 그대로 넣는 값 |
hangul.covered | int | 한글 음절 11,172자 중 실제로 가진 글자 수. 부족하면 없는 글자가 □ 로 깨집니다 |
webfont.subset | bool | unicode-range 조각 서빙 여부. false 면 글자 하나만 써도 파일 전체를 받습니다 |
webfont.sample_load_bytes | int | 200자 표준 문단을 렌더할 때 실제로 전송되는 바이트. 폰트 간 비교용 고정 기준입니다 |
line_height.recommended | float | 이 값을 CSS line-height 에 넣으면 어디서 열어도 줄간격이 같습니다. 폰트 안 높이 값(hhea·typo·win)에서 계산합니다 |
line_height.consistent | bool | false 면 line-height 를 안 적었을 때 맥과 윈도우에서 줄간격이 다르게 잡힙니다 (spread_em 만큼) |
css | string | <link> 에 바로 넣는 웹폰트 CSS 주소 |
호출 예시
// 한글 전체 지원 + 가벼운 폰트만 골라내기 const r = await fetch('https://textyle.net/api/fonts'); const { fonts } = await r.json(); const safe = fonts.filter(f => f.hangul?.percent === 100 && // 두부 안 나는 폰트 f.webfont?.sample_load_bytes < 150_000 // 본문 150KB 미만 ); console.log(safe.map(f => f.ko));
curl https://textyle.net/api/fonts/noto-sans-kr
요청 제한
IP당 시간당 300회 · 매 정시 초기화인증이 없는 공개 API라 서버 보호를 위해 IP 기준으로 제한합니다. 모든 응답에 남은 호출 수가 헤더로 붙으니 미리 확인하고 조절할 수 있습니다.
| 헤더 | 의미 |
|---|---|
X-RateLimit-Limit | 시간당 허용 횟수 (300) |
X-RateLimit-Remaining | 이번 시간에 남은 횟수 |
X-RateLimit-Reset | 초기화 시각 (Unix 타임스탬프, 다음 정시) |
Retry-After | 429 일 때만 — 몇 초 뒤 재시도하면 되는지 |
304 응답은 한도에서 차감하지 않습니다. 아래처럼 ETag 를 재사용하면 사실상 제한 없이 폴링할 수 있습니다 — 캐시를 잘 쓰는 쪽에 불이익이 없도록 한 설계입니다. 한도를 넘기면 429 와 함께 {"error":"rate_limited"} 가 돌아옵니다.
let etag = null;
async function poll() {
const r = await fetch('https://textyle.net/api/fonts', {
headers: etag ? { 'If-None-Match': etag } : {}
});
console.log('남은 호출', r.headers.get('X-RateLimit-Remaining'));
if (r.status === 304) return null; // 변경 없음 — 한도 차감 안 됨
if (r.status === 429) { // 한도 초과
const wait = +r.headers.get('Retry-After');
console.warn(`${wait}초 뒤 재시도`);
return null;
}
etag = r.headers.get('ETag');
return (await r.json()).fonts;
}
캐시
목록·상세 모두 Cache-Control: public, max-age=120 이고 ETag 가 붙습니다. 목록 응답은 서버에서도 120초간 캐시되므로, 같은 시간대의 반복 호출은 항상 같은 결과를 즉시 받습니다. 조회수·좋아요가 최대 120초 늦게 반영될 수 있다는 뜻이기도 합니다.
자주 묻는 질문
API 키가 필요한가요?
아니요. 인증 없이 바로 호출할 수 있습니다. 다만 서버 보호를 위해 IP당 시간당 300회 제한이 있습니다.
브라우저에서 바로 부를 수 있나요?
네. Access-Control-Allow-Origin: * 이라 어느 도메인에서도 fetch 로 호출됩니다. 남은 호출 수도 X-RateLimit-Remaining 헤더로 읽을 수 있습니다.
한도에 걸리면 어떻게 되나요?
429 와 Retry-After 를 돌려줍니다. 매 정시에 초기화되며, ETag 로 304 를 받은 요청은 한도에서 차감하지 않습니다.
응답이 자주 바뀌나요?
폰트 목록은 자동 발견 파이프라인이 월·수·금 새벽에 갱신합니다. 목록 응답은 서버에서 120초간 캐시됩니다.