favicon.ico를 app 디렉토리에 추가하고 작동하게 만드는 것은 쉽습니다. 하지만 마케팅 페이지와 대시보드에 서로 다른 아이콘이 필요하거나, 페이지 리로드 없이 사용자의 테마 설정에 맞춰 아이콘을 바꿔야 할 때는 어떻게 해야 할까요? 파일 기반 규약은 편리하지만 모든 상황을 커버하기는 어렵습니다.

Next.js는 메타데이터 객체 내부에 icons 필드를 제공하여 아이콘을 프로그래밍 방식으로 선언할 수 있습니다. 이 방식을 사용하면 layout.tsx나 특정 page.tsx에서 rel, sizes, type, 심지어 media 속성까지 세밀하게 제어할 수 있습니다. 이 글에서는 정적 파일에서 메타데이터 기반 설정으로 마이그레이션하는 실용적인 단계를 살펴보겠습니다.

사전 준비 사항

Step 1: 재사용 가능한 아이콘 설정 정의

레이아웃 전체에서 동일한 아이콘 정의를 재사용할 수 있도록 작은 헬퍼를 만드세요. 이렇게 하면 메타데이터 exports가 깔끔해지고 나중에 업데이트하기도 쉬워집니다.

// lib/icons.ts
import { Metadata } from 'next';

export const iconConfig = {
  icon: [
    { url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
    { url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
    { url: '/icons/icon.svg', sizes: 'any', type: 'image/svg+xml' },
  ],
  apple: [
    { url: '/icons/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
  ],
  shortcut: '/icons/favicon.ico',
  other: [
    {
      rel: 'mask-icon',
      url: '/icons/safari-pinned-tab.svg',
      color: '#0066cc',
    },
  ],
} satisfies Metadata['icons'];

Step 2: 루트 레이아웃에 설정 적용

헬퍼를 불러와 메타데이터 export의 icons 필드에 전개 연산자로 넣으세요. 이렇게 하면 app/ 디렉토리에 직접 두었던 favicon.ico, icon.png, apple-icon.png 파일이 더 이상 필요하지 않습니다.

// app/layout.tsx
import type { Metadata } from 'next';
import { iconConfig } from '@/lib/icons';

export const metadata: Metadata = {
  title: 'My App',
  icons: iconConfig,
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body>{children}</body>
    </html>
  );
}

Step 3: 특정 라우트의 아이콘 재정의

메타데이터는 라우트 트리 아래로 병합되므로, 페이지에서 루트 아이콘 세트를 확장하거나 교체할 수 있습니다. 이 기능은 블로그 하위 디렉토리나 테넌트 대시보드처럼 섹션별 브랜딩이 필요한 경우에 유용합니다.

// app/dashboard/layout.tsx
import type { Metadata } from 'next';
import { iconConfig } from '@/lib/icons';

export const metadata: Metadata = {
  icons: {
    ...iconConfig,
    icon: [
      { url: '/icons/dashboard-32x32.png', sizes: '32x32', type: 'image/png' },
      { url: '/icons/dashboard.svg', sizes: 'any', type: 'image/svg+xml' },
    ],
  },
};

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <section>{children}</section>;
}

Step 4: 데이터 기반 아이콘 동적 생성

기능 플래그, 사용자 역할, A/B 변형에 따라 아이콘이 바뀌어야 한다면 서버 컴포넌트에서 데이터를 가져와 icons 객체를 즉석에서 만드세요. 아래 예제는 theme 쿠키가 다크 모드를 나타낼 때 고대비 변형을 선택합니다.

// app/settings/page.tsx
import { cookies } from 'next/headers';
import type { Metadata } from 'next';

export async function generateMetadata(): Promise<Metadata> {
  const cookieStore = await cookies();
  const theme = cookieStore.get('theme')?.value ?? 'light';
  const suffix = theme === 'dark' ? '-dark' : '';

  return {
    icons: {
      icon: [
        { url: `/icons/settings${suffix}-32x32.png`, sizes: '32x32' },
        { url: `/icons/settings${suffix}.svg`, sizes: 'any' },
      ],
      apple: [{ url: `/icons/settings${suffix}-apple.png`, sizes: '180x180' }],
    },
  };
}

export default function SettingsPage() {
  return &lt;main&gt;Settings&lt;/main&gt;;
}

Step 5: 미디어 쿼리를 사용한 다크 모드 SVG 추가

CSS로 적응하는 단일 SVG를 선호한다면 한 번만 선언하고 media 속성으로 나머지를 처리하세요. 이렇게 하면 중복 리소스를 피하고 head를 가볍게 유지할 수 있습니다.

// lib/icons.ts (추가)
export const adaptiveIconConfig = {
  icon: [
    {
      url: '/icons/adaptive.svg',
      sizes: 'any',
      type: 'image/svg+xml',
      media: '(prefers-color-scheme: light)',
    },
    {
      url: '/icons/adaptive-dark.svg',
      sizes: 'any',
      type: 'image/svg+xml',
      media: '(prefers-color-scheme: dark)',
    },
  ],
} satisfies Metadata['icons'];

흔한 함정

확인 체크리스트

  1. DevTools → Network → img 필터를 열어 선언된 각 아이콘이 200을 반환하는지 확인하세요.
  2. head 내부에 link rel='icon', link rel='apple-touch-icon', link rel='mask-icon' 태그가 있는지 검사하세요.
  3. OS 설정에서 다크 모드 전환을 테스트하세요. 새로고침 없이 올바른 SVG가 로드되어야 합니다.
  4. Mzu favicondl의 온라인 체커로 빠른 감사를 실행하여 누락된 크기나 MIME 타입 불일치를 잡아내세요.

파일 기반 규약을 고수해야 할 때

프로젝트에 단일 정적 favicon 세트만 필요하다면 파일 기반 접근 방식(app/favicon.ico, app/icon.png 등)이 여전히 가장 간단한 선택입니다. TypeScript도 런타임도 필요 없으며 별도 설정 없이 작동합니다. 라우트별 재정의, 동적 선택, media와 color 같은 고급 속성이 필요할 때 메타데이터 API를 활용하세요.

파일 기반 워크플로에 대해 더 자세히 알아보려면 Next.js App Router favicon 추가 가이드를 참고하세요. PWA를 구축 중이라면 Web App Manifest favicon 가이드에서 이 메타데이터 선언과 함께 필요한 매니페스트 아이콘을 다룹니다.

문서화된 요구 사항 확인

Next.js는 메타데이터 파일 규약 문서에서 favicon과 앱 아이콘을 다루고 있습니다.