favicon.ico를 app 디렉토리에 추가하고 작동하게 만드는 것은 쉽습니다. 하지만 마케팅 페이지와 대시보드에 서로 다른 아이콘이 필요하거나, 페이지 리로드 없이 사용자의 테마 설정에 맞춰 아이콘을 바꿔야 할 때는 어떻게 해야 할까요? 파일 기반 규약은 편리하지만 모든 상황을 커버하기는 어렵습니다.
Next.js는 메타데이터 객체 내부에 icons 필드를 제공하여 아이콘을 프로그래밍 방식으로 선언할 수 있습니다. 이 방식을 사용하면 layout.tsx나 특정 page.tsx에서 rel, sizes, type, 심지어 media 속성까지 세밀하게 제어할 수 있습니다. 이 글에서는 정적 파일에서 메타데이터 기반 설정으로 마이그레이션하는 실용적인 단계를 살펴보겠습니다.
사전 준비 사항
- App Router를 사용하는 Next.js 13 이상 프로젝트
- public/ 디렉토리나 접근 가능한 CDN 경로에 배치된 아이콘 리소스(SVG, PNG, ICO)
- next의 Metadata 타입에 대한 familiarity
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 <main>Settings</main>;
}
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'];
흔한 함정
- satisfies 제약 조건 잊기. 이것이 없으면 TypeScript가 누락된 url이나 잘못된 rel 값을 잡아내지 못합니다.
- 파일 기반 아이콘과 메타데이터 아이콘 혼용. Next.js는 둘을 병합하지만, 중복된 rel 항목이 브라우저 동작을 예측 불가능하게 만들 수 있습니다. 라우트 세그먼트별로 한 가지 전략을 선택하세요.
- 선행 슬래시 없이 상대 URL 사용. 항상 /로 시작하거나 절대 CDN 출처를 사용하세요. 그렇지 않으면 브라우저가 현재 경로명을 기준으로 URL을 해석합니다.
- 대체 ICO 생략. 일부 엔터프라이즈 프록시와 구형 크롤러는 여전히 /favicon.ico를 요청합니다. 설정에 shortcut: '/icons/favicon.ico'를 유지하세요.
확인 체크리스트
- DevTools → Network → img 필터를 열어 선언된 각 아이콘이 200을 반환하는지 확인하세요.
- head 내부에 link rel='icon', link rel='apple-touch-icon', link rel='mask-icon' 태그가 있는지 검사하세요.
- OS 설정에서 다크 모드 전환을 테스트하세요. 새로고침 없이 올바른 SVG가 로드되어야 합니다.
- 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과 앱 아이콘을 다루고 있습니다.