Vite로 새 Vue 3 프로젝트를 생성하고 개발 서버를 띄운 뒤 브라우저 탭을 살펴보면, 브랜드 아이콘이 있어야 할 자리에 기본 Vite 로고가 자리 잡고 있습니다. 해결책은 프로젝트의 public/ 폴더에 있으며, Vite 관련 함정만 알고 나면 올바르게 연결하는 데 약 3분 정도면 충분합니다.

사전 요구 사항

Vite 기반의 Vue 3 프로젝트와 함께, 적용하려는 favicon 파일이 필요합니다. 아직 준비하지 않았다면 Mzu favicondl을 사용해 ICO, PNG, SVG로 구성된 전체 번들을 생성해 즉시 사용할 수 있도록 다운로드하시기 바랍니다. webpack 관련 특이 사항이나 copy-webpack-plugin 규칙은 필요 없습니다. Vite는 별도의 설정 없이 public 폴더를 정적 자산으로 처리합니다.

1단계: public/ 폴더 열기

모든 Vue + Vite 스캐폴드에는 프로젝트 루트에 public/ 디렉터리가 포함되어 있습니다. Vite의 public 디렉터리 문서에 따르면, 여기에 배치한 파일은 변환 없이 빌드 출력으로 복사되어 루트 경로에서 제공됩니다.

즉, public/favicon.ico는 /favicon.ico로 접근할 수 있으며, /public/favicon.ico로는 접근할 수 없습니다. 이 차이를 놓치는 것이 Vue 애플리케이션이 프로덕션 환경에서 깨진 탭 아이콘을 그대로 노출하는 가장 흔한 원인입니다.

2단계: Favicon 파일 배치하기

전체 favicon 패키지를 public/ 폴더에 직접 배치합니다.

public/
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
├── site.webmanifest
└── favicon.svg

Vite는 폴더 전체를 그대로 dist/ 출력으로 복사합니다. import나 변환 과정이 발생하지 않습니다. Vue 컴포넌트에서 이 파일들을 import하지 마시기 바랍니다. 번들 외부에 두는 것이 올바른 방식입니다. 오늘날 실제로 의미 있는 사이즈에 대한 자세한 분석은 Favicon Sizes Guide에서 확인하실 수 있습니다.

3단계: index.html에 link 태그 추가하기

프로젝트 루트의 index.html 파일을 엽니다. Vite가 Vue 앱을 마운트하는 바로 그 파일입니다. <head> 안에 link rel="icon" 선언을 추가합니다.

<head>
  <meta charset="UTF-8" />
  <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
  <link rel="icon" href="/favicon-32x32.png" type="image/png" sizes="32x32" />
  <link rel="icon" href="/favicon.ico" sizes="any" />
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
  <link rel="manifest" href="/site.webmanifest" />
</head>

여기서 순서가 중요합니다. 브라우저는 지원 가능한 첫 번째 rel="icon" 항목을 선택하기 때문에 SVG를 가장 앞에 두고 선명한 스케일링을 확보한 다음 PNG 폴백을 이어서 배치하고, 레거시 ICO는 마지막에 둡니다. 모든 경로는 /로 시작해야 하며 /public/로 시작해서는 안 됩니다.

4단계: 다크 모드 SVG 지원 추가하기

다크 모드 전환을 적용하고 싶다면 Vue의 reactivity를 연결하기보다 SVG 내부에 media query를 직접 삽입하는 방식을 권장합니다.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    circle { fill: #1a1a1a; }
    @media (prefers-color-scheme: dark) {
      circle { fill: #fafafa; }
    }
  </style>
  <circle cx="16" cy="16" r="14" />
</svg>

브라우저가 자동으로 fill 값을 전환합니다. 탭에서 watch() 호출이나 lifecycle hook이 필요하지 않습니다. 패턴에 대한 자세한 내용은 Favicon Dark Mode Guide에서 확인하실 수 있습니다.

5단계: 개발 및 프로덕션 환경에서 검증하기

npm run dev를 실행하고 다음 세 가지를 확인합니다.

  1. DevTools → Network → "favicon" 필터에서 모든 파일이 200 OK로 반환되는지 확인합니다.
  2. 브라우저 탭에 기본 Vite 표시가 아닌 사용자의 아이콘이 표시되는지 확인합니다.
  3. curl http://localhost:5173/favicon.svg 명령이 SVG 바이트를 반환하는지 확인합니다.

이어서 npm run build를 실행하고 dist/를 검사합니다. public/의 모든 파일이 변경 없이 dist/ 루트에 나타나야 합니다. 프로덕션 환경에서 누락된 파일이 있다면 호스팅의 rewrite 규칙이 경로를 제거하고 있을 가능성이 높으므로 vercel.json 또는 _redirects를 확인하시기 바랍니다.

자주 발생하는 문제

마무리

Vue + Vite의 public 폴더 워크플로우는 현대 웹 개발에서 가장 깔끔한 favicon 설정 방식 중 하나입니다. 파일을 배치하고 index.html에 link 태그를 연결하기만 하면 Vite가 모든 것을 배치한 위치 그대로 dist/로 복사합니다. 플러그인 설정, head 주입 관련 특이 사항, Vue Router의 요청 가로채기 없이 오직 루트 경로에서 제공되는 정적 자산만으로 구성됩니다.

Mzu favicondl로 완전한 favicon 패키지를 생성하고 link 태그를 index.html에 붙여 넣으면 Vue 앱이 모든 브라우저 탭에서 완성도 있게 보이게 됩니다.