next/og 아이콘을 폰트 주입 없이 굽기 — SVG data-URI 트릭

Satori 는 시스템 폰트 폴백이 없어 넘긴 fonts 에 없는 글자를 두부로 그린다. 아이콘이 글자가 아니라 도형이면 폰트가 아예 필요 없다.

app/apple-icon.tsx 나 OG 이미지를 next/ogImageResponse 로 만들 때, 그림에 글자를 쓰면 곤란해진다.

한글이나 특수기호를 넣으면 두부(.notdef □) 가 나온다. Satori — next/og 내부 렌더러 — 는 시스템 폰트 폴백이 없기 때문이다. fonts 옵션으로 넘긴 폰트에 해당 코드포인트가 없으면 빈 상자를 그린다.

그래서 아이콘마다 OTF 를 읽어 주입하게 되고, 커버리지(cmap) 관리가 따라붙는다. 아이콘 하나 굽자고 폰트 로딩 코드가 생긴다.

아이콘이 도형이면 폰트가 필요 없다

배경은 CSS 그라데이션으로, 그림은 SVG 를 data-URI <img> 로 얹는다. Satori 가 resvg 로 래스터화한다.

// app/apple-icon.tsx
import { ImageResponse } from "next/og";
export const size = { width: 180, height: 180 };
export const contentType = "image/png";

// utf8 data-URI 안에서는 '#' 를 %23 로 이스케이프해야 색이 먹는다
const HEART =
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 29.6"><path fill="%23fff" d="M23.6 0c-3.4 0-6.3 2.7-7.6 5.6C14.7 2.7 11.8 0 8.4 0 3.8 0 0 3.8 0 8.4c0 9.4 9.5 11.9 16 21.2 6.1-9.3 16-12.1 16-21.2C32 3.8 28.2 0 23.6 0z"/></svg>';

export default function AppleIcon() {
  return new ImageResponse(
    (
      <div style={{ width:"100%", height:"100%", display:"flex", alignItems:"center",
        justifyContent:"center", background:"linear-gradient(135deg,#ff7ab6,#c2005e)" }}>
        <img width={96} height={89} alt="" src={`data:image/svg+xml;utf8,${HEART}`} />
      </div>
    ),
    size
  );
}

fonts 옵션 자체가 없다. OTF 로드도, cmap 필터도 필요 없다.

세 가지를 지켜야 동작한다

# 은 반드시 %23. data:image/svg+xml;utf8, 안에서 # 은 프래그먼트 구분자로 해석된다. 이스케이프하지 않으면 거기서 URI 가 끊겨 색과 경로가 통째로 깨진다. 가장 많이 밟는 지점이다.

<img>width / height 를 명시한다. Satori 는 브라우저처럼 내재 크기를 계산해주지 않는다. 안 주면 크기를 못 잡는다.

빌드 후 산출물 크기를 확인한다. .next/server/app/apple-icon.body 가 수 KB PNG 면 정상이다. 0B 나 수십 B 면 렌더 실패다. 빌드는 성공하고 아이콘만 안 나오는 상태라 이 확인이 없으면 배포하고 나서 안다.

SVG 하나로 favicon 과 manifest 를 같이 덮는다

글자 없는 벡터라면 app/icon.svg 정적 파일 하나로 많은 게 정리된다.

  • Next 가 자동으로 <link rel="icon" type="image/svg+xml"> 을 추가한다. 모던 브라우저는 이걸 쓴다.

  • app/manifest.tsicons 에서 재사용한다.

    { src: "/icon.svg", sizes: "any", type: "image/svg+xml", purpose: "maskable" }

    사이즈별 PNG 를 여러 장 만들 필요가 없다.

두 가지 예외가 있다.

Apple 은 SVG apple-touch-icon 을 무시한다. 그래서 위처럼 apple-icon.tsx 로 PNG 를 따로 구워야 한다. 이 한 장 때문에 ImageResponse 를 쓰는 셈이다.

maskable 은 안전영역을 지켜야 한다. 핵심 그림을 중앙 55% 안에 둔다. 플랫폼이 바깥을 잘라낼 수 있다.

폰트가 여전히 필요한 경우

아이콘에 진짜 글자가 들어가야 하면 — 한글 워드마크 같은 것 — 폰트는 불가피하다. 그때는 OTF 를 모듈 스코프에 캐시하고, cmap 으로 커버리지를 검사해서 두부를 미리 막는다.

다만 도형으로 끝낼 수 있으면 이 트릭이 훨씬 가볍다. 로딩 코드도, 커버리지 검사도, 폰트 파일도 없다.

정리

Satori 를 “브라우저처럼 그려주는 것”으로 생각하면 계속 어긋난다. 폰트 폴백이 없고 내재 크기 계산도 없다. 필요한 걸 명시적으로 다 넘겨줘야 한다.

그렇다면 방향은 반대로 잡는 게 낫다. 명시할 게 가장 적은 표현을 고르는 것. 아이콘에서 그건 대체로 글자가 아니라 도형이었다.