
# 로고를 앱에 붙이기 — 복사해서 붙이는 레시피들

> 로고가 끝내 들어가는 흔한 형태들을 위한 실용적 스니펫들: `<img>` 태그, React 컴포넌트, Next.js Image, Tailwind 아바타 슬롯, 다크 모드 인식 `<picture>` 엘리먼트, OG 이미지 폴백, 그리고 서버 서명 URL. 모든 스니펫은 공개 `https://api.clearlogo.dev` 엔드포인트에서 작동한다.
>
> 모든 예제는 브라우저 키(클라이언트 코드용) 또는 서버 키(백엔드 코드용)를 가지고 있다고 가정한다. [키를 만들자](https://clearlogo.dev/login?lang=ko) 대시보드에서.

## 1. 순수 HTML

가장 빠른 통합 — URL을 `<img>` 태그에 붙여넣자.

```html
<img
  src="https://api.clearlogo.dev/logo/github.com?size=64&content=80&token=YOUR_BROWSER_KEY"
  alt="GitHub"
  width="64"
  height="64"
/>
```

`size`를 렌더링 크기와 맞춰서 설정하면 브라우저가 더 큰 자산을 스케일링하느라 대역폭을 낭비하지 않는다.

## 2. React 컴포넌트

작은 재사용 가능한 컴포넌트는 대부분의 제품 UI 요구를 충족한다.

```tsx
type CompanyLogoProps = {
  domain: string;
  size?: 32 | 48 | 64 | 96 | 128;
  alt?: string;
};

const BROWSER_KEY = process.env.NEXT_PUBLIC_CLEARLOGO_KEY!;

export function CompanyLogo({ domain, size = 64, alt }: CompanyLogoProps) {
  return (
    <img
      src={`https://api.clearlogo.dev/logo/${domain}?size=${size}&content=80&token=${BROWSER_KEY}`}
      alt={alt ?? `${domain} logo`}
      width={size}
      height={size}
      loading="lazy"
      decoding="async"
    />
  );
}
```

`loading="lazy"`와 `decoding="async"`는 긴 목록(CRM, 디렉터리, 계정 테이블)이 페인트를 블로킹하지 않게 유지한다.

## 3. Next.js `<Image>`

`next.config.js`에 API 호스트를 한 번 추가하고, 최적화된 `<Image>` 컴포넌트를 어디서나 사용하자.

```js
// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "api.clearlogo.dev" },
    ],
  },
};
```

```tsx
import Image from "next/image";

export function CompanyLogo({ domain, size = 64 }: { domain: string; size?: number }) {
  return (
    <Image
      src={`https://api.clearlogo.dev/logo/${domain}?size=${size * 2}&content=80&token=${process.env.NEXT_PUBLIC_CLEARLOGO_KEY}`}
      alt={`${domain} logo`}
      width={size}
      height={size}
      unoptimized
    />
  );
}
```

HiDPI 디스플레이에서 선명함을 위해 `size * 2`를 요청하자. API의 WebP 협상이 클라이언트에 흐르도록 하려면 `unoptimized`를 사용하자. Next.js가 포맷 협상을 자체적으로 처리하게 하려면 제거하자.

## 4. Tailwind 아바타 슬롯

로고를 고정 UI 슬롯에 붙여서 브랜드 간 시각적 일관성을 유지한다.

```tsx
<div className="flex items-center gap-3">
  <div className="h-10 w-10 overflow-hidden rounded-md bg-neutral-100 ring-1 ring-neutral-200">
    <img
      src={`https://api.clearlogo.dev/logo/${domain}?size=64&content=80&token=${BROWSER_KEY}`}
      alt=""
      className="h-full w-full object-contain"
    />
  </div>
  <div>
    <div className="font-medium">{name}</div>
    <div className="text-sm text-neutral-500">{domain}</div>
  </div>
</div>
```

`object-contain` 더하기 고정 슬롯 크기는 테이블과 목록을 위한 가장 신뢰할 수 있는 형태다. `content=80` 파라미터와 쌍을 이루면 로고가 절대 슬롯 가장자리에 닿지 않는다.

## 5. 다크 모드 인식 로고

`<picture>`를 `prefers-color-scheme`과 함께 사용해 브라우저가 페인트 시간에 다크 변형을 교환하게 하자 — JavaScript 없음, 리렌더 없음, 목록 안에서 로고당 훅이 발동하지 않음.

```html
<picture>
  <source
    srcset="https://api.clearlogo.dev/logo/github.com?size=64&theme=dark&token=YOUR_BROWSER_KEY"
    media="(prefers-color-scheme: dark)"
  />
  <img
    src="https://api.clearlogo.dev/logo/github.com?size=64&theme=light&token=YOUR_BROWSER_KEY"
    alt="GitHub"
    width="64"
    height="64"
  />
</picture>
```

React 컴포넌트로 감싸자:

```tsx
export function CompanyLogo({ domain, size = 64 }: { domain: string; size?: number }) {
  const base = `https://api.clearlogo.dev/logo/${domain}?size=${size}&content=80&token=${BROWSER_KEY}`;
  return (
    <picture>
      <source srcSet={`${base}&theme=dark`} media="(prefers-color-scheme: dark)" />
      <img src={`${base}&theme=light`} alt={`${domain} logo`} width={size} height={size} />
    </picture>
  );
}
```

다크 버전이 없을 때 API는 라이트 변형으로 폴백하므로, `theme=dark`를 요청하는 것은 항상 안전하다. 앱 수준 테마 스위치(OS 수준이 아닌)의 경우, `prefers-color-scheme` 대신 당신의 테마 상태에서 소스 URL을 구동하자.

## 6. 플레이스홀더를 가진 목록 렌더링

긴 목록의 경우, 빈 슬롯이 깜빡이지 않도록 즉시 플레이스홀더를 렌더링하자.

```tsx
function LogoCell({ domain }: { domain: string }) {
  const [loaded, setLoaded] = useState(false);
  return (
    <div className="relative h-10 w-10 rounded-md bg-neutral-100">
      <img
        src={`https://api.clearlogo.dev/logo/${domain}?size=64&content=80&token=${BROWSER_KEY}`}
        alt=""
        loading="lazy"
        onLoad={() => setLoaded(true)}
        className={`h-full w-full object-contain transition-opacity ${
          loaded ? "opacity-100" : "opacity-0"
        }`}
      />
    </div>
  );
}
```

## 7. 첫 글자로 에러 폴백

API가 도메인의 로고를 찾을 수 없으면, 결정론적 글자 아바타를 렌더링하자.

```tsx
function LogoOrFallback({ domain, name }: { domain: string; name: string }) {
  const [errored, setErrored] = useState(false);

  if (errored) {
    return (
      <div className="flex h-10 w-10 items-center justify-center rounded-md bg-neutral-200 font-medium text-neutral-600">
        {name[0]?.toUpperCase() ?? "?"}
      </div>
    );
  }

  return (
    <img
      src={`https://api.clearlogo.dev/logo/${domain}?size=64&content=80&token=${BROWSER_KEY}`}
      alt={`${name} logo`}
      width={40}
      height={40}
      className="rounded-md"
      onError={() => setErrored(true)}
    />
  );
}
```

폴백은 네트워크를 요청하지 않으므로, 콜드 행도 빠르게 유지된다.

## 8. Open Graph 이미지 폴백

서버사이드 페치를 사용해 당신의 동적 생성 OG 카드에 로고를 삽입하자.

```ts
// app/og/route.ts (Next.js, Edge runtime)
import { ImageResponse } from "next/og";

export const runtime = "edge";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const domain = searchParams.get("domain") ?? "example.com";

  const logoUrl = `https://api.clearlogo.dev/logo/${domain}?size=256&content=80&token=YOUR_SERVER_KEY`;

  return new ImageResponse(
    (
      <div style={{ display: "flex", alignItems: "center", padding: 64 }}>
        <img src={logoUrl} width={128} height={128} alt="" />
        <span style={{ marginLeft: 32, fontSize: 64 }}>{domain}</span>
      </div>
    ),
    { width: 1200, height: 630 },
  );
}
```

여기서는 브라우저 키가 아닌 **서버** 키를 사용하자 — Edge 함수는 서버사이드이며, 서버 키는 제한 없는 오리진 헤더를 견뎌낸다.

## 9. 민감한 UI를 위한 백엔드 서명

URL을 공개하고 싶지 않을 때(예: 내부 관리자 도구), 백엔드를 통해 프록시하자.

```ts
// app/api/logo/[domain]/route.ts (Next.js)
export async function GET(
  request: Request,
  { params }: { params: { domain: string } },
) {
  const upstream = await fetch(
    `https://api.clearlogo.dev/logo/${params.domain}?size=128&content=80`,
    { headers: { Authorization: `Bearer ${process.env.CLEARLOGO_SERVER_KEY}` } },
  );
  return new Response(upstream.body, {
    headers: {
      "Content-Type": upstream.headers.get("Content-Type") ?? "image/png",
      "Cache-Control": "public, max-age=86400, s-maxage=86400",
    },
  });
}
```

페이지 소스에 API 키 없음, 그리고 당신의 CDN에서 캐싱을 통제한다.

## 10. TypeScript 헬퍼

단일 헬퍼는 코드베이스 전체에서 URL 구성을 일관되게 유지한다.

```ts
type LogoOptions = {
  size?: 16 | 32 | 48 | 64 | 96 | 128 | 192 | 256 | 512 | 1024;
  content?: number; // 50–100, step 5
  theme?: "light" | "dark";
  format?: "png" | "webp" | "jpeg";
};

export function logoUrl(domain: string, opts: LogoOptions = {}): string {
  const params = new URLSearchParams({
    size: String(opts.size ?? 64),
    content: String(opts.content ?? 80),
    token: process.env.NEXT_PUBLIC_CLEARLOGO_KEY!,
  });
  if (opts.theme) params.set("theme", opts.theme);
  if (opts.format) params.set("format", opts.format);
  return `https://api.clearlogo.dev/logo/${encodeURIComponent(domain)}?${params}`;
}
```

`encodeURIComponent`는 도메인의 특이한 문자에 대해 보호하고 헬퍼를 신뢰할 수 없는 입력으로 호출하기에 안전하게 만든다.

## 11. 연결을 미리 준비하도록 preconnect

대시보드가 fold 위에 수십 개의 로고를 렌더링한다면, 첫 번째 요청은 DNS, TCP, TLS 비용을 낸다. 첫 번째 `<img>`가 요청되기 전에 연결이 준비되도록 단일 `<link rel="preconnect">`를 당신의 문서 헤드에 추가하자 — 그 다음 모든 로고는 로고당 preload 대역폭 낭비 없이 헤드 스타트를 얻는다.

```html
<link rel="preconnect" href="https://api.clearlogo.dev" crossorigin />
<link rel="dns-prefetch" href="https://api.clearlogo.dev" />
```

```tsx
// Next.js
import Head from "next/head";

export function ClearLogoPreconnect() {
  return (
    <Head>
      <link rel="preconnect" href="https://api.clearlogo.dev" crossOrigin="" />
      <link rel="dns-prefetch" href="https://api.clearlogo.dev" />
    </Head>
  );
}
```

`preconnect`는 DNS를 해석하고 TCP/TLS 핸드셰이크를 즉시 열고, `dns-prefetch`는 preconnect를 무시하는 브라우저를 위한 저렴한 폴백이다. 페이지가 렌더링하는 로고 수가 몇 개든 총 두 개의 힌트. 로고당 `<link rel="preload" as="image">`보다 이를 선호하자 — preload는 무겁고, 대역폭 예산에 계산되며, 렌더링된 URL이 preload된 것과 약간 다르면(크기, 테마, 토큰) 도움이 되지 않는다.

## FAQ

### 어떤 레시피로 시작해야 하나?

단일 로고를 렌더링한다면, 순수 `<img>` 레시피면 충분하다. 목록과 테이블의 경우, Tailwind 아바타 슬롯과 레이지로드 패턴으로 시작하자.

### 테스트에 키가 필요한가?

아니다. 엔드포인트는 저용량 브라우징을 위해 익명으로 작동한다. 요청이 제대로 귀속되고 당신의 플랜에 대해 계산되도록 프로덕션에 출시하기 전에 브라우저 키를 추가하자.

### 어떤 크기를 요청해야 하나?

CSS의 렌더링 크기와 일치시키고, 그 후 망막 선명함을 위해 `2x`를 요청하자. 32픽셀 슬롯에 `1024`를 요청하지 말자 — 대역폭이 들고 더 좋아 보이지 않는다.

### JavaScript 없이 다크 모드를 어떻게 처리하나?

`media="(prefers-color-scheme: dark)"`로 게이팅된 `<source>`를 가진 `<picture>` 엘리먼트를 사용하자. 브라우저가 페인트 시간에 올바른 URL을 선택한다 — React 훅 없음, 리렌더 없음, 긴 목록에서도 행당 오버헤드 없음.

### API가 로고를 반환하지 않을 때 폴백을 표시하나?

`<img>`의 `onError`를 청취하고 글자 아바타나 일반 아이콘으로 바꾸자. 위의 에러 폴백 레시피는 프로덕션 형태다.

### 서버사이드 렌더에서 브라우저 키를 사용할 수 있나?

할 수 있다 — 하지만 서버 키가 더 나은 선택이다. 왜냐하면 오리진 체크에 의존하기보다는 `Authorization: Bearer …`를 통해 요청을 보내기 때문이다. Next.js Server Components, OG 라우트, 또는 백엔드 프록시의 경우, 서버 키를 사용하자.

### 당신의 CDN에서 로고를 어떻게 캐시하나?

위의 백엔드 서명 레시피를 사용하자. 업스트림 응답은 오래 생존하는 `Cache-Control`을 포함하므로, 당신의 프록시에서 같은 헤더를 설정하고 당신의 CDN에 나머지를 하게 하자.
