파비콘 이미지 파일 하나 만드는 것도 귀찮아서 코드로 해결한 적이 있는데, 알고 보니 두 프로젝트 다 그렇게 하고 있더라고요. img-compressormedia-cropicon.tsx, opengraph-image.tsx를 비교해볼게요. 둘 다 이미지 파일 대신 코드로 아이콘이랑 OG 이미지를 만들어요.

정적 이미지 파일 대신 컴포넌트로 아이콘을 만들어요

img-compressor 파비콘은 이렇게 JSX를 그대로 ImageResponse에 넘겨서 만들어요.

import { ImageResponse } from 'next/og';

export const runtime = 'edge';
export const size = { width: 32, height: 32 };
export const contentType = 'image/png';

export default function Icon() {
  return new ImageResponse(
    (
      
🖼️
), { ...size } ); }

media-crop은 방식은 같은데 이모지 대신 lucide 스타일 SVG 패스를 직접 그려서 브랜드 아이콘을 만들어요. 둘 다 export const sizeexport const contentType을 같이 선언해서, Next.js가 빌드할 때 메타 태그랑 실제 PNG 응답을 제대로 이어줄 수 있게 해요.

edge 런타임은 왜 꼭 명시해야 했나

두 파일 다 export const runtime = 'edge'를 선언해요. ImageResponse는 내부적으로 Satori라는 걸로 JSX를 SVG로, 다시 PNG로 바꾸는데, 이 변환이 Node.js 전용 API 없이도 엣지 환경에서 돌아가도록 만들어져 있더라고요. 런타임을 명시 안 하면 배포 환경에 따라 아이콘 생성 함수가 엉뚱한 런타임에서 돌 수도 있어요.

일반 CSS 레이아웃이랑 다른 점

ImageResponse 안의 style은 진짜 DOM에 렌더링되는 게 아니라 Satori가 해석하는 제한된 flexbox 서브셋이에요. 그래서 두 프로젝트 아이콘 코드 다 display: flex랑 고정값 위주 스타일만 쓰고, 일반 웹페이지에서 쓰는 grid, gap, 외부 CSS 클래스는 안 써요. 이 제약을 모르고 평소 쓰던 컴포넌트 스타일을 그대로 옮기면 레이아웃이 깨지더라고요.

OG 이미지 크기는 프로젝트마다 다르게 선언해요. img-compressor의 opengraph-image.tsx는 소셜 미리보기용으로 더 큰 캔버스를 쓰고, 파비콘용 icon.tsx는 32×32처럼 작게 써요. 같은 ImageResponse API를 쓰더라도 용도에 맞게 크기랑 폰트 크기를 따로 정해야 실제로 배치했을 때 안 잘리고 안 흐려지더라고요.