튜토리얼

색상 API 설계: 색상 데이터용 REST 끝점

8분 읽기

변환이 필요한 디자인 도구, 대비를 확인하는 접근성 검사기, 명명된 색상 검색이 필요한 색상 선택기 구성 요소, 조화를 생성하는 팔레트 생성기 등 얼마나 많은 도구가 색상 데이터를 소비하는지 생각하기 전까지 색상 API는 틈새 시장으로 들립니다. 이러한 항목을 구축하거나 다른 개발자에게 색상 데이터를 제공하려면 잘 설계된 REST API가 필요합니다.

이 가이드는 ColorFYI에서 색상 변환기대비 검사기를 구동하는 끝점을 구축한 사람의 관점에서 스키마 결정, 끝점 구조, 응답 형식, 오류 처리 및 속도 제한 등 전체 디자인 표면을 다룹니다.


색상 데이터를 위한 API 스키마 디자인

핵심 색상 개체

색상에는 16진수, RGB, HSL, CMYK, OKLCH 등 다양한 표현과 여러 메타데이터 필드가 있습니다. 표준 응답 개체에는 해당 항목이 모두 포함되어 클라이언트가 추가 요청 없이 필요한 형식을 사용할 수 있도록 해야 합니다.

{
  "hex": "#FF5733",
  "rgb": {
    "r": 255,
    "g": 87,
    "b": 51,
    "css": "rgb(255, 87, 51)"
  },
  "hsl": {
    "h": 11,
    "s": 100,
    "l": 60,
    "css": "hsl(11, 100%, 60%)"
  },
  "hsv": {
    "h": 11,
    "s": 80,
    "v": 100
  },
  "cmyk": {
    "c": 0,
    "m": 66,
    "y": 80,
    "k": 0
  },
  "oklch": {
    "l": 0.63,
    "c": 0.19,
    "h": 27.5,
    "css": "oklch(0.63 0.19 27.5)"
  },
  "luminance": 0.215,
  "is_dark": false,
  "name": "Coral Red",
  "name_source": "color_name_database",
  "name_distance": 0.04
}

is_dark(휘도에서 파생됨)를 포함하면 클라이언트가 WCAG 휘도 계산을 직접 구현하지 않아도 됩니다. namename_distance를 포함하면 클라이언트는 별도의 API 호출 없이 가장 가까운 이름의 색상을 표시할 수 있습니다.

명명 규칙

JSON 키에는 snake_case를 사용합니다(대부분의 REST API와 일치). 관련 필드를 평면화(rgb_r, rgb_g, rgb_b)하는 대신 객체(rgb, hsl, cmyk)에 중첩합니다. 중첩된 객체는 JavaScript에서 더 읽기 쉽고, 구조 해제가 더 쉬우며, 유형이 지정된 인터페이스에 깔끔하게 매핑됩니다.

interface ColorResponse {
  hex: string;
  rgb: { r: number; g: number; b: number; css: string };
  hsl: { h: number; s: number; l: number; css: string };
  oklch: { l: number; c: number; h: number; css: string };
  luminance: number;
  is_dark: boolean;
  name: string | null;
  name_source: string | null;
  name_distance: number | null;
}

16진수 정규화

# 유무, 3자리 또는 6자리, 대문자 또는 소문자 등 다양한 형식의 16진수 색상을 허용하고 처리하기 전에 API 레이어에서 정규화합니다.

# Python/Django example
import re

def normalize_hex(raw: str) -> str:
    """Normalize hex input to 6-digit uppercase without #."""
    clean = raw.strip().lstrip('#').upper()
    if not re.match(r'^[0-9A-F]{3}$|^[0-9A-F]{6}$', clean):
        raise ValueError(f"Invalid hex color: {raw!r}")
    # Expand 3-digit to 6-digit
    if len(clean) == 3:
        clean = ''.join(c * 2 for c in clean)
    return clean

응답에 정규화된 형식을 반환합니다. 이렇게 하면 모호성이 제거되고 다운스트림 캐싱을 더 예측하기 쉬워집니다.


Hex/RGB/HSL 변환 끝점

GET /api/color/{hex}

기본 엔드포인트는 지정된 16진수 코드에 대한 전체 색상 개체를 반환합니다. 이는 색상 변환기가 모든 형식 필드를 동시에 채우는 데 사용하는 것입니다.

GET /api/color/FF5733
GET /api/color/%23FF5733   (URL-encoded #)
GET /api/color/f57          (3-digit, lowercase)

세 가지 모두 #FF5733에 대해 동일한 응답을 반환해야 합니다.

응답:

{
  "hex": "#FF5733",
  "rgb": { "r": 255, "g": 87, "b": 51, "css": "rgb(255, 87, 51)" },
  "hsl": { "h": 11, "s": 100, "l": 60, "css": "hsl(11, 100%, 60%)" },
  "cmyk": { "c": 0, "m": 66, "y": 80, "k": 0 },
  "oklch": { "l": 0.63, "c": 0.19, "h": 27.5, "css": "oklch(0.63 0.19 27.5)" },
  "luminance": 0.215,
  "is_dark": false,
  "name": "Coral Red",
  "name_source": "css_extended",
  "name_distance": 0.08
}

POST /api/변환

16진수가 아닐 수 있는 임의의 입력을 변환하는 경우 — CSS 색상 이름, RGB 트리플, HSL 값:

POST /api/convert
Content-Type: application/json

{
  "input": "rgb(255, 87, 51)",
  "from": "rgb"
}

응답: 동일한 풀 컬러 개체입니다.

hex, rgb, hsl, cmyk, oklch, namefrom 값을 허용합니다. 입력 형식이 선언된 from 유형과 일치하는지 확인하고 불일치 시 명확한 오류 메시지와 함께 422를 반환합니다.

Django ViewSet 예

# apps/api/views.py
from django.http import JsonResponse
from django.views import View
from apps.colors.engine import ColorEngine

class ColorDetailView(View):
    def get(self, request, hex_code: str):
        try:
            normalized = normalize_hex(hex_code)
        except ValueError as e:
            return JsonResponse(
                {"error": "invalid_hex", "message": str(e)},
                status=422
            )

        engine = ColorEngine(normalized)
        data = {
            "hex": f"#{normalized}",
            "rgb": engine.to_rgb_dict(),
            "hsl": engine.to_hsl_dict(),
            "cmyk": engine.to_cmyk_dict(),
            "oklch": engine.to_oklch_dict(),
            "luminance": engine.relative_luminance(),
            "is_dark": engine.is_dark(),
            "name": engine.nearest_name(),
            "name_source": engine.nearest_name_source(),
            "name_distance": engine.nearest_name_distance(),
        }
        response = JsonResponse(data)
        response["Cache-Control"] = "public, max-age=86400, stale-while-revalidate=604800"
        return response

색상 데이터를 적극적으로 캐시합니다. 주어진 16진수 코드는 항상 동일한 출력을 생성하므로 캐시의 수명이 매우 길어질 수 있습니다.


색상 검색 및 자동 완성 API

GET /api/검색

명명된 색상 검색은 색상 선택기 및 검색 필드에서 자동 완성 기능을 제공합니다. 쿼리 매개변수는 부분 색상 이름입니다.

GET /api/search?q=coral
GET /api/search?q=teal&limit=5
GET /api/search?q=%230d&source=css   // Search by hex prefix

응답:

{
  "results": [
    {
      "hex": "#FF7F50",
      "name": "Coral",
      "source": "css_named",
      "score": 1.0
    },
    {
      "hex": "#FF6B6B",
      "name": "Light Coral",
      "source": "css_extended",
      "score": 0.85
    },
    {
      "hex": "#E8735A",
      "name": "Terra Cotta",
      "source": "crayola",
      "score": 0.62
    }
  ],
  "total": 3,
  "query": "coral"
}

검색 구현 고려 사항

작은 이름의 색상 데이터베이스(최대 2,000개 항목)의 경우 퍼지 일치를 사용한 메모리 내 접두사 검색이 충분히 빠릅니다.

from difflib import SequenceMatcher

def search_colors(query: str, limit: int = 10) -> list[dict]:
    q = query.lower().strip()
    results = []

    for color in NAMED_COLORS:  # Pre-loaded list
        name = color["name"].lower()
        if q in name:
            # Exact substring match: score based on position
            pos = name.index(q)
            score = 1.0 - (pos / len(name)) * 0.3
        else:
            # Fuzzy match
            score = SequenceMatcher(None, q, name).ratio()
            if score < 0.5:
                continue

        results.append({**color, "score": round(score, 3)})

    results.sort(key=lambda x: x["score"], reverse=True)
    return results[:limit]

항목이 10,000개보다 큰 데이터베이스의 경우 밀리초 미만 퍼지 검색을 위한 트라이그램 인덱싱 기능이 있는 PostgreSQL의 pg_trgm 확장을 사용하세요.

-- Enable extension
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Add index
CREATE INDEX idx_color_name_trgm ON named_colors USING gin(name gin_trgm_ops);

-- Query
SELECT hex, name, source, similarity(name, 'coral') AS score
FROM named_colors
WHERE name % 'coral'
ORDER BY score DESC
LIMIT 10;

자동완성 최적화

입력 시 실시간 검색 자동 완성을 위해 짧은 대기 시간을 위해 최적화하세요.

  1. 클라이언트에서 디바운스 — 요청을 실행하기 전 최소 150ms입니다.
  2. 최소 쿼리 길이 설정 — 2자보다 짧은 쿼리를 거부합니다(빈 결과가 즉시 반환됨).
  3. 쿼리별로 결과 캐시 — 다른 사용자의 동일한 쿼리는 동일한 결과를 반환합니다. Cache-Control: public, max-age=3600를 사용하세요.
  4. 결과가 없으면 빠르게 반환 — 전체 퍼지 스캔을 실행하는 대신 {"results": [], "total": 0}로 즉시 응답합니다.

대비 검사 끝점

GET /api/대조

대비 API는 대비 검사기 뒤에 있는 엔진입니다. 두 개의 16진수 코드를 사용하고 각 레벨에 대한 통과/실패 상태와 함께 WCAG 명암비를 반환합니다.

GET /api/contrast?fg=FF5733&bg=FFFFFF
GET /api/contrast?fg=000000&bg=FFFFFF

응답:

{
  "ratio": 3.02,
  "ratio_formatted": "3.02:1",
  "foreground": {
    "hex": "#FF5733",
    "luminance": 0.215
  },
  "background": {
    "hex": "#FFFFFF",
    "luminance": 1.0
  },
  "wcag": {
    "aa_normal": false,
    "aa_large": true,
    "aaa_normal": false,
    "aaa_large": false
  },
  "level": "AA Large",
  "passes": true
}

level 필드는 쌍이 달성하는 가장 높은 WCAG 수준("AAA", "AA", "AA Large" 또는 "Fail")을 반환합니다. WCAG 수준이 충족되면 passes 필드는 true입니다. 간단한 녹색/빨간색 표시기에 유용합니다.

대비 계산

def relative_luminance(hex_code: str) -> float:
    r, g, b = hex_to_rgb(hex_code)
    def linearize(v: int) -> float:
        s = v / 255
        return s / 12.92 if s <= 0.04045 else ((s + 0.055) / 1.055) ** 2.4
    return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)

def contrast_ratio(hex1: str, hex2: str) -> float:
    L1 = relative_luminance(hex1)
    L2 = relative_luminance(hex2)
    lighter = max(L1, L2)
    darker = min(L1, L2)
    return round((lighter + 0.05) / (darker + 0.05), 2)

def wcag_result(ratio: float) -> dict:
    return {
        "aa_normal": ratio >= 4.5,
        "aa_large": ratio >= 3.0,
        "aaa_normal": ratio >= 7.0,
        "aaa_large": ratio >= 4.5,
    }

접근 가능한 대안 제안

유용한 확장: 배경과 대비가 맞지 않는 전경 색상이 있는 경우 WCAG AA를 달성하는 최소 밝기 조정을 제안합니다.

def suggest_accessible_foreground(fg_hex: str, bg_hex: str, target_ratio: float = 4.5) -> str:
    """Return a lightness-adjusted version of fg_hex that passes target_ratio."""
    hsl = rgb_to_hsl(*hex_to_rgb(fg_hex))
    bg_lum = relative_luminance(bg_hex)
    is_dark_bg = bg_lum < 0.179

    # Try adjusting lightness in the direction that increases contrast
    step = -2 if is_dark_bg else 2  # Darken on light bg, lighten on dark bg
    current_l = hsl[2]

    for _ in range(50):
        current_l = max(0, min(100, current_l + step))
        adjusted_hex = hsl_to_hex(hsl[0], hsl[1], current_l)
        ratio = contrast_ratio(adjusted_hex, bg_hex)
        if ratio >= target_ratio:
            return adjusted_hex

    # Return maximum contrast if target is unreachable
    return '#000000' if not is_dark_bg else '#FFFFFF'

Color API의 속도 제한

Color API에 속도 제한이 필요한 이유

Color API는 위험도가 낮은 것처럼 보일 수 있지만 다음과 같은 대상이 될 수 있습니다. - 열거 공격: 전체 색상 이름 데이터베이스를 수집하기 위해 1,670만 개의 16진수 코드를 모두 반복합니다. - 스크래핑: 변환 논리를 무료 계산 리소스로 사용합니다. - 악용: 다른 클라이언트의 서비스 수준을 저하시키는 단일 클라이언트의 대량 요청입니다.

실행 전략

Django API의 경우 django-ratelimit는 Redis 백엔드에 데코레이터 기반 속도 제한을 제공합니다.

# settings.py
RATELIMIT_USE_CACHE = 'default'  # Uses Django's cache backend (Redis recommended)

# views.py
from django_ratelimit.decorators import ratelimit

@ratelimit(key='ip', rate='100/m', method='GET', block=True)
def color_detail_view(request, hex_code: str):
    ...

속도 제한 응답 형식

속도 제한이 초과되면 명확한 응답 본문 및 Retry-After 헤더와 함께 429 Too Many Requests를 반환합니다.

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1708012345
Content-Type: application/json

{
  "error": "rate_limit_exceeded",
  "message": "You have exceeded the rate limit of 100 requests per minute.",
  "retry_after": 60
}

계층화된 비율 제한

합리적인 계층 구조:

계층 한도 열쇠
익명 요청 60개/분 IP 주소
API 키(무료) 요청 300개/분 API 키
API 키(유료) 요청 3000개/분 API 키

API 키는 Authorization: Bearer {key} 또는 X-API-Key: {key}로 전달됩니다. 인증된 요청에 대한 비율 제한 키로 API 키를 사용합니다.

@ratelimit(key='header:x-api-key', rate='300/m', method='GET')
@ratelimit(key='ip', rate='60/m', method='GET')
def color_detail_view(request, hex_code: str):
    # django-ratelimit checks both; the looser limit wins for authenticated requests
    ...

응답 형식 모범 사례

일관된 오류 스키마

모든 오류 응답(검증 실패, 속도 제한, 찾을 수 없음)은 동일한 스키마를 따라야 합니다.

{
  "error": "invalid_hex",
  "message": "The value 'gggggg' is not a valid hex color. Expected a 3 or 6 digit hex string.",
  "field": "hex_code",
  "value": "gggggg"
}
필드 목적
error 기계가 읽을 수 있는 오류 코드(snake_case)
message 사람이 읽을 수 있는 설명
field 오류를 일으킨 입력 필드(유효성 검사 오류의 경우)
value 잘못된 값(디버깅에 도움)

컬렉션 끝점에 대한 페이지 매김

limitoffset를 사용한 검색 엔드포인트:

{
  "results": [...],
  "pagination": {
    "total": 47,
    "limit": 10,
    "offset": 0,
    "next": "/api/search?q=blue&limit=10&offset=10",
    "prev": null
  }
}

클라이언트가 전체 결과 세트를 알지 못해도 페이지 매김 컨트롤을 렌더링할 수 있도록 항상 total를 포함하십시오.

버전 관리 전략

헤더가 아닌 URL 경로에서 API 버전을 지정하세요. URL 버전 관리는 명시적이며 CDN에서 캐시할 수 있으며 클라이언트가 사용자 정의 헤더를 설정할 필요가 없습니다.

/api/v1/color/FF5733    # Version 1
/api/v2/color/FF5733    # Version 2 (when breaking changes are needed)

주요 버전 출시 후 최소 1년 동안 이전 버전을 유지합니다. 이전 버전에서는 지원 중단 헤더를 사용하세요.

Deprecation: Sun, 01 Jan 2026 00:00:00 GMT
Sunset: Sun, 01 Jan 2027 00:00:00 GMT
Link: </api/v2/color/FF5733>; rel="successor-version"

CORS 구성

브라우저 클라이언트가 사용하는 공개 API의 경우:

# settings.py (using django-cors-headers)
CORS_ALLOWED_ORIGINS = [
    "https://yoursite.com",
    "https://app.yoursite.com",
]

# Or for a fully public API:
CORS_ALLOW_ALL_ORIGINS = True

# Always restrict methods and headers
CORS_ALLOW_METHODS = ['GET', 'POST', 'OPTIONS']
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization', 'X-API-Key']

캐싱 아키텍처

색상 데이터는 캐시 가능성이 높습니다. #FF5733의 색상 개체는 절대 변경되지 않습니다. 변환 수학은 결정적입니다. 그에 따라 캐시 전략을 구성하십시오.

엔드포인트 캐시 제어 CDN TTL
/api/color/{hex} public, max-age=86400, stale-while-revalidate=604800 1일
/api/search?q={q} public, max-age=3600 1시간
/api/contrast?fg=X&bg=Y public, max-age=86400 1일
/api/palette/{hex} public, max-age=86400 1일

조건부 요청에 대한 16진수 코드를 기반으로 ETag 헤더를 추가합니다.

response["ETag"] = f'"{hex_code}"'
response["Last-Modified"] = "Thu, 01 Jan 2026 00:00:00 GMT"  # Static — content never changes

If-None-Match: "FF5733"를 보내는 클라이언트는 본문 없이 304 Not Modified 응답을 받습니다. 이는 반복 요청 시 대역폭을 절약합니다.


주요 내용

  • 표준 색상 객체는 단일 응답에 모든 형식 표현(16진수, RGB, HSL, CMYK, OKLCH)을 포함해야 합니다. 클라이언트가 다른 형식을 얻기 위해 여러 요청을 하도록 만들지 마십시오.
  • 16진수 입력을 적극적으로 정규화합니다. #를 제거하고 3자리 숫자를 대문자로 확장합니다. 처리하기 전에 정규식으로 유효성을 검사하세요.
  • 대비 끝점은 모든 접근성 도구의 핵심입니다. 즉, WCAG 비율, 수준별 통과/실패 및 단일 응답의 최고 통과 수준을 반환합니다.
  • 명명된 색상 검색은 소규모 데이터베이스에 대한 메모리 내 접두어 검색과 함께 안정적으로 작동합니다. 대규모에는 PostgreSQL pg_trgm를 사용하십시오.
  • 인증되지 않은 요청에 대한 IP별 속도 제한(60/min이 합리적임) 상위 계층에는 API 키를 사용하세요.
  • 모든 오류 응답은 최소한 error(기계 판독 가능) 및 message(사람 판독 가능)와 동일한 스키마를 따라야 합니다.
  • 색상 데이터는 캐시 가능성이 높습니다. 24시간 CDN 캐시는 변환 엔드포인트에 적합합니다. 효율적인 캐시 무효화를 위해 ETagstale-while-revalidate를 사용하십시오.
  • 색상 변환기대비 검사기를 사용하여 이러한 API 엔드포인트가 실제로 작동하는지 확인하세요.

관련 색상

관련 브랜드

관련 도구