뒤로

통합 요리책

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

회사 로고를 UI에 추가하는 프로덕션 레디 스니펫들. 순수 img 태그부터 React, Next.js Image, Tailwind 아바타, 다크 모드 처리, OG 폴백, 서버사이드 서명까지.

로고가 끝내 들어가는 흔한 형태들을 위한 실용적 스니펫들: <img> 태그, React 컴포넌트, Next.js Image, Tailwind 아바타 슬롯, 다크 모드 인식 <picture> 엘리먼트, OG 이미지 폴백, 그리고 서버 서명 URL. 모든 스니펫은 공개 https://api.clearlogo.dev 엔드포인트에서 작동한다.

모든 예제는 브라우저 키(클라이언트 코드용) 또는 서버 키(백엔드 코드용)를 가지고 있다고 가정한다. 키를 만들자 대시보드에서.

1. 순수 HTML

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

<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 요구를 충족한다.

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> 컴포넌트를 어디서나 사용하자.

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "api.clearlogo.dev" },
    ],
  },
};
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 슬롯에 붙여서 브랜드 간 시각적 일관성을 유지한다.

<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 없음, 리렌더 없음, 목록 안에서 로고당 훅이 발동하지 않음.

<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 컴포넌트로 감싸자:

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. 플레이스홀더를 가진 목록 렌더링

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

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가 도메인의 로고를 찾을 수 없으면, 결정론적 글자 아바타를 렌더링하자.

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 카드에 로고를 삽입하자.

// 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을 공개하고 싶지 않을 때(예: 내부 관리자 도구), 백엔드를 통해 프록시하자.

// 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 구성을 일관되게 유지한다.

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 대역폭 낭비 없이 헤드 스타트를 얻는다.

<link rel="preconnect" href="https://api.clearlogo.dev" crossorigin />
<link rel="dns-prefetch" href="https://api.clearlogo.dev" />
// 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에 나머지를 하게 하자.

playground에서 실제 도메인을 시도해보자

레시피 형태가 맞으면, 당신의 고객이나 파트너 도메인 몇 개를 바꿔 넣고 출시 전에 결과를 미리보자.