새로 만든 React + Vite 앱을 배포하고 favicon.ico를 public/에 넣은 뒤 dev 서버를 재시작했는데, 브라우저 탭에는 여전히 Vite 번개 아이콘이 보입니다. 심지어 아무것도 표시되지 않는 경우도 있죠. 이는 React Vite favicon이 표시되지 않는 고전적인 문제로, 거의 항상 몇 가지 특정 설정 오류 중 하나로 귀결됩니다.
먼저 2분 만에 해결하는 방법을 알려드리고, 이어서 근본 원인들을 자세히 설명하겠습니다.
2분 만에 해결하기
프로젝트 루트의 index.html을 열고 link 태그의 경로가 /public/이 아닌 루트 /로 시작하는 절대 경로인지 확인합니다. Vite에서 public/ 디렉터리의 파일은 빌드 출력으로 복사되어 루트 경로에서 제공됩니다(Vite public 디렉터리 문서 참조). 따라서 public/favicon.ico의 favicon은 /favicon.ico로 참조해야 합니다.
<link rel='icon' type='image/x-icon' href='/favicon.ico' />
<link rel='icon' type='image/svg+xml' href='/icon.svg' />
<link rel='apple-touch-icon' href='/apple-touch-icon.png' />
파일을 저장합니다. Vite가 실행 중이라면 HMR이 자동으로 반영합니다. 그래도 아이콘이 표시되지 않으면 강제 새로 고침(Ctrl+Shift+R 또는 Cmd+Shift+R)을 수행하고 Network 탭에서 favicon URL의 응답 코드가 200인지 확인합니다.
근본 원인 분석
1. 파일이 src/에 있고 public/에 없는 경우
가장 흔한 실수입니다. src/의 파일은 Vite의 번들러를 거치며 모듈로 import되어 파일명에 해시가 붙습니다. 반면 public/의 파일은 그대로 루트 경로에서 제공됩니다. src/assets/에 favicon을 두고 /favicon.ico 경로에 표시되길 기대하면 해당 경로에는 파일이 존재하지 않습니다. public/로 이동시키면 됩니다.
2. link 태그의 경로가 잘못된 경우
Create React App에서 넘어온 개발자들은 가끔 href='%PUBLIC_URL%/favicon.ico' 또는 href='/public/favicon.ico'처럼 작성합니다. Vite에는 %PUBLIC_URL% 변수가 존재하지 않으며, /public/는 런타임에 존재하지 않는 디렉터리 이름입니다. 평범한 /favicon.ico를 사용해야 합니다.
3. base 경로 불일치
vite.config.js에서 base: '/my-app/'를 설정했다면, 모든 asset 경로에 해당 prefix를 포함해야 합니다. href='/favicon.ico'로 작성한 link 태그는 앱이 https://example.com/my-app/에서 제공될 때 404를 반환합니다. 상대 경로(href='./favicon.ico')를 사용하거나, 설정한 base를 포함하도록 모든 경로를 수정하면 됩니다.
4. 캐시가 잘못된 정보를 보여주는 경우
브라우저는 favicon을 매우 공격적으로 캐시합니다. Chrome은 favicon을 일반 캐시 삭제 후에도 살아남는 별도의 SQLite 데이터베이스에 저장합니다(이 부분은 favicon 캐시 삭제 가이드에서 다룬 바 있습니다). 경로를 수정한 뒤에도 이전 깨진 아이콘이 몇 시간 동안 그대로 남아 있을 수 있습니다. 개발 중에 쿼리 스트링 버전을 변경하면 새로운 요청을 강제로 만들 수 있습니다.
<link rel='icon' href='/favicon.ico?v=2' />
5. SPA 라우팅이 /favicon.ico를 가로채는 경우
React Router에서 catch-all 라우트를 사용 중이라면, /favicon.ico 요청이 React 앱에 의해 처리되어 아이콘 대신 HTML을 반환할 수 있습니다. Vite의 dev 서버는 public/ 파일에 우선순위를 두므로 이 문제는 주로 프로덕션 빌드에서 발생합니다. 배포된 URL을 curl로 요청해 Content-Type 헤더를 확인하면 검증할 수 있습니다.
6. React 컴포넌트 내부의 link 태그
JSX 컴포넌트 안에 배치한 <link> 태그가 항상 예상대로 <head>에 렌더링되지는 않습니다. React는 link 태그를 자동으로 끌어올리지 않으며, 컴포넌트가 DOM에 마운트될 때에만 적용됩니다. 동적 아이콘이 필요하다면 react-helmet-async를 사용하고, 정적인 favicon 태그는 index.html에 직접 작성해야 합니다.
7. SVG favicon 구문 오류
Chrome은 rel='icon'에 대한 HTML 명세를 엄격하게 따라 SVG favicon을 파싱합니다. namespace 선언 누락, 잘못된 viewBox 속성, 외부 리소스 참조는 조용히 실패합니다. 참조하기 전에 브라우저 탭에서 SVG를 직접 열어 테스트해 보시기 바랍니다.
예방 체크리스트
- favicon 파일은
public/에만 둡니다.src/에는 절대 두지 않습니다. - 루트에서 시작하는 절대 경로(
/favicon.ico)로 참조합니다. - 모든
<link rel='icon'>태그는 JSX가 아닌index.html에 작성합니다. - 개발 중에는 버전 쿼리 스트링을 추가해 캐시를 무효화합니다.
curl -I https://yoursite.com/favicon.ico명령으로 배포된 빌드를 검증합니다.- 아이콘이 깨졌다고 판단하기 전에 시크릿 창에서 테스트합니다.
Vite의 파일 기반 메타데이터 접근 방식에 대한 자세한 내용은 Vite public 폴더 favicon 완전 가이드를 참고하시기 바랍니다. 그리고 이슈가 React에 한정된 것(Helmet, 동적 아이콘 등)이라면 React favicon 설정 가이드에서 해당 패턴을 다루고 있습니다.
마무리
React Vite favicon 미표시 문제는 거의 항상 파일 위치, 경로 참조, 캐시 문제로归结됩니다. 이 세 가지만 제대로 처리하면 90%는 해결됩니다. 나머지 10%는 보통 base 경로 불일치이거나 React 컴포넌트가 link 태그를 흡수하는 경우인데, 이 두 가지는 DevTools에서 명확한 흔적을 남깁니다.
Mzu favicondl로 전체 아이콘 세트를 생성하고, 파일들을 public/에 넣은 뒤, link 태그를 index.html에 붙여 넣은 다음 배포하면 됩니다.