shields.io 대안으로 배지를 shadcn/ui 버튼으로 렌더링했습니다

발행: (2026년 4월 26일 AM 07:02 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

README 배지는 10년 동안 똑같은 모습이었습니다: 평평한 직사각형, 기본 색상, 클래식한 shields.io 미학. 작동은 하지만, shadcn/ui나 최신 컴포넌트 라이브러리를 사용할 때 배지는 항상 어울리지 않는 느낌이 납니다.

저는 모든 것이 같은 디자인 시스템에 속한 것처럼 보이는 배지를 원했습니다. 그래서 shieldcn 를 만들었습니다.

무엇을 하는가

모든 배지는 shadcn/ui Button 컴포넌트를 Satori 로 SVG로 렌더링한 실제 버튼입니다.

  • 동일한 Inter 폰트
  • 변형별 동일한 border‑radius, padding, 색상 토큰

URL을 받아 README에 삽입하면 버튼처럼 보입니다.

![npm](https://shieldcn.dev/npm/react.svg)
![stars](https://shieldcn.dev/github/stars/vercel/next.js.svg)
![discord](https://shieldcn.dev/discord/1316199667142496307.svg)

모든 shadcn Button 변형이 작동합니다: default, secondary, outline, ghost, destructive. 또한 아이콘의 브랜드 색상을 자동으로 가져오는 branded 변형도 있습니다.

![branded](https://shieldcn.dev/npm/react.svg?variant=branded)
![outline](https://shieldcn.dev/npm/react.svg?variant=outline)
![ghost](https://shieldcn.dev/npm/react.svg?variant=ghost)

How it works

<svg> 태그로 삽입된 SVG는 완전히 샌드박스됩니다—외부 스타일시트, CSS 변수, JavaScript가 전혀 사용되지 않습니다. 모든 색상은 렌더링 전에 리터럴 헥스 값으로 변환되어야 합니다.

shadcn Button 토큰을 모두 추출해 조회 테이블에 넣었습니다:

export const darkMode: ModeColors = {
  primary: "#fafafa",
  primaryForeground: "#18181b",
  secondary: "#27272a",
  secondaryForeground: "#fafafa",
  destructive: "#dc2626",
  // …
}

단일 resolve() 함수가 변형, 크기, 모드, 테마 및 색상 오버라이드를 받아 모든 값을 계산하고 렌더러에 전달합니다. 렌더러 자체는 변형당 분기문이 전혀 없으며—헥스 값만 받아 배지를 배치합니다.

// resolve() computes ALL colors before rendering
const resolved = resolve(config)

// One render path for every variant
const svg = await renderSingle(resolved)

새 변형을 추가한다는 것은 토큰 테이블에 행을 하나 추가하는 것뿐이며, 렌더링 로직은 그대로 유지됩니다.

Satori 특이점

opacity CSS 속성 없음

Satori는 opacity를 조용히 무시합니다. 대신 내장 알파가 포함된 rgba()를 사용하세요:

function rgba(hex: string, opacity: number): string {
  const h = hex.replace("#", "")
  const r = parseInt(h.substring(0, 2), 16)
  const g = parseInt(h.substring(2, 4), 16)
  const b = parseInt(h.substring(4, 6), 16)
  return `rgba(${r},${g},${b},${opacity})`
}

dangerouslySetInnerHTML 사용 불가

모든 SVG 아이콘은 Satori가 렌더링하기 전에 React 요소 트리로 파싱되어야 합니다. 저는 원시 SVG 문자열을 중첩된 <svg>, <path>, <g> 등 요소로 변환하는 가벼운 SVG 파서를 작성했습니다.

폰트 로딩 중요

Next.js Route Handler에서는 URL에서 폰트를 가져오지 말고 readFileSync를 사용해 파일 시스템에서 폰트를 로드해야 합니다. 저는 모듈 스코프에서 모든 폰트 파일을 미리 로드하여 요청 간에 캐시되도록 했습니다.

아키텍처

전체 애플리케이션은 단일 Next.js 캐치‑올 라우트에 존재합니다:

app/[...slug]/route.ts

URL을 provider + params 형태로 파싱하고, 데이터를 가져와 색상을 결정한 뒤 배지를 렌더링하며 SVG(또는 @resvg/resvg-wasm을 통한 PNG, JSON)를 반환합니다.

lib/providers/에 제공자 함수들이 위치하며, 각 함수는 동일한 형태를 반환합니다:

{
  label: string,
  value: string,
  color?: string,
  link?: string
}

렌더러는 데이터가 어디서 왔는지 신경 쓰지 않고, 단순히 라벨, 값, 그리고 색상 몇 개를 받아서 처리합니다.

다루는 내용

현재 25개 이상의 데이터 제공자:

  • 패키지 레지스트리 – npm, PyPI, Crates.io, Docker Hub, Packagist, RubyGems, NuGet, Pub.dev, Homebrew, Maven, CocoaPods, JSR, Bundlephobia
  • 코드 플랫폼 – GitHub (stars, CI, issues, PRs, releases, downloads, license, …), Codecov, VS Code Marketplace
  • 소셜 – Discord, Reddit, Bluesky, YouTube, Mastodon, Lemmy, Hacker News
  • 맞춤형 – 정적 배지, 동적 JSON (任意 API 지정), HTTPS 엔드포인트 프록시, 메모 배지 (PUT으로 직접 데이터 입력)

40 000개 이상의 아이콘 은 SimpleIcons, Lucide, React Icons에서 제공됩니다. 또한 base64 데이터 URI를 통해 맞춤 SVG를 업로드할 수 있습니다.

Token pool

GitHub의 인증되지 않은 API 제한은 시간당 60 요청 — 배지 서비스에는 전혀 부족합니다. Shields.io는 사용자가 OAuth 토큰을 기부하는 토큰 풀로 이 문제를 해결했습니다. 저도 같은 접근 방식을 차용했습니다.

  • 사용자는 GitHub OAuth 앱(읽기 전용, 스코프 없음, 언제든지 취소 가능)을 승인합니다.
  • 그들의 토큰은 Postgres에 저장된 풀에 추가됩니다.
  • API 요청은 풀에 있는 모든 토큰에 걸쳐 분산됩니다.
  • 토큰이 많을수록 용량이 늘어납니다.

shadcn 레지스트리

또한 컴포넌트 레지스트리 가 있어, 자체 앱에서 배지 컴포넌트를 사용하고 싶다면:

pnpm dlx shadcn@latest add "https://shieldcn.dev/r/readme-badge.json"
pnpm dlx shadcn@latest add "https://shieldcn.dev/r/readme-badge-row.json"
pnpm dlx shadcn@latest add "https://shieldcn.dev/r/badge-preview.json"

Try it

  • Homepage + badge builder:
  • Docs:
  • GitHub:

MIT 라이선스이며, 모든 것이 오픈 소스입니다. 즐기세요!

Hing은 무료이며, PR을 환영합니다. 여러분이 사용해 주시면 좋겠습니다.

0 조회
Back to Blog

관련 글

더 보기 »

영화 친구

개요: 이것은 OpenClaw Writing Challenge에 대한 제출물입니다. DEV 커뮤니티에서 오랫동안 구경만 하고 글을 읽어온 저는, 마침내 하나에 도전해 보았습니다.