아이콘(SVG → React 컴포넌트) 렌더링 실패 트러블슈팅
1. 개요 (Summary)
Vite + React + vite-plugin-svgr 환경에서 SVG 아이콘이 React 컴포넌트로 렌더링되지 않고
- JSX 사용 시
JSX 요소 형식 'X'에 구문 또는 호출 시그니처가 없습니다.ts(2604) 타입 오류
- 자동 ICONS 맵이 비거나 “등록 불가” 경고
가 발생했다.
문제는 단일 원인이 아니라, “설정/타입/로직/캐시” 4가지 축이 복합적으로 얽혀 진단을 어렵게 만들었다.
2. 최초 증상 (Initial Symptoms)
| 관찰 | 상세 |
|---|
| 타입 오류 | <ArchiveUp /> 사용 시 TS2604 (함수형 컴포넌트로 인식되지 않음) |
| 런타임 500 | 특정 시점에 Icons.ts 요청이 500 (내부 변환 중 예외) |
| ICONS 맵 비었음 | 자동 수집된 ICONS 객체에 키가 없음 또는 경고 다수 |
| typeof 결과 | typeof ArchiveUp === 'string' (URL) 로 출력되는 경우 발생 |
| ReactComponent 네임드 export | 기대했던 ReactComponent 키 부재 |
3. 초기 가설 (Hypotheses Considered)
| 번호 | 가설 | 근거 | 우선순위 | 실제 여부 |
|---|
| H1 | svgr 플러그인 설정/순서 문제 | typeof 가 string, ReactComponent 없음 | 높음 | 일부 (원인 구성요소) |
| H2 | svg 타입 선언(.d.ts) 누락/오염 | TS2604 / string 추론 | 높음 | 실제 |
| H3 | ICONS 빌드 로직 결함 | 맵 비었음 / 경고 | 중간 | 실제 |
| H4 | 캐시(node_modules/.vite) 영향 | 설정 바꿔도 변화 없음 | 중간 | 실제(지연 요인) |
| H5 | 파일 인코딩/주석 깨짐 영향 | 한글 깨짐(�) | 낮음 | 부차적 (주 원인 아님) |
| H6 | 순환 의존성 | 맵 비거나 undefined | 낮음 | 최종 미확인 (가능성 낮음) |
| H7 | JSX/TSX 확장자 문제 | .ts 에 JSX 사용 시 | 낮음 | 이 케이스에서는 해당 없음 |
4. 시도했던 해결 단계 & 결과 (Experiments)
| 실험 | 조치 | 기대 | 실제 결과 | 판정 |
|---|
| E1 | 단일 아이콘 직접 import (import ArchiveUp from ...) | 함수형 컴포넌트 확인 | typeof 가 string 인 케이스 존재 | 원인 계속 |
| E2 | ReactComponent 네임드 import 시도 | 네임드 export 존재 확인 | undefined / 키 부재 | svgr 미적용/순서 의심 강화 |
| E3 | ICONS 생성 루프에서 mod.ReactComponent 만 체크 | 정상 등록 | 비거나 경고 | default 처리 필요 확인 |
| E4 | 루프에 Object.keys(mod) 로그 추가 | 모듈 구조 파악 | keys: "default" | svgr 또는 순서 문제 확증 |
| E5 | svgr 플러그인 옵션 추가 (exportType: 'named') | ReactComponent 제공 | 타입 오류 지속 | 타입 선언 오염 또는 순서 문제 남음 |
| E6 | vite.config.ts 에 선언 유지한 채 캐시 삭제 | 타입 반영 기대 | 변화 없음 | 선언 위치 자체 문제 |
| E7 | declare module 들을 별도 svg.d.ts 로 이동 | 타입 인식 정상화 | TS2604 감소 (일부 여전) | 절반 해결 |
| E8 | 플러그인 순서 react() → svgr() → svgr() → react() 변경 | 변환 성공 | ReactComponent 인식, typeof function | 핵심 조합 해결 |
| E9 | ICONS 로직에 default 함수 fallback 추가 | 맵 채워짐 | 키 목록 채워짐 | 성공 |
| E10 | node_modules/.vite 삭제 후 재기동 | 이전 캐시 무효화 | 변환/타입 일관 | 잔여 이상 증상 제거 |
5. 근본 원인 (Root Cause Analysis)
| 축 | 상세 |
|---|
| 타입 선언(Declaration Pollution) | 전역 타입이어야 할 declare module '*.svg' 블록을 vite.config.ts 내부에 배치 → TS 서버가 이를 안정적으로 전역 인식하지 못하거나 잘못된 string 추론 고착. |
| 플러그인 순서(Transformation Order) | react() 가 svgr() 앞에 위치 → SVG 가 JSX 로 바뀌기 전에 React 플러그인의 JSX 처리가 지나가 변환 결과가 URL 로 남음. |
| ICONS 생성 로직(Logic Gap) | ReactComponent 만 검사하고 default 함수/URL fallback 고려 부족 → 변환 성공 사례도 맵에 미등록. |
| 빌드 캐시(Cache Persistence) | .vite 캐시가 이전 asset 처리 방식을 유지 → 설정 수정 후 즉시 효과 확인 실패(오판 유발). |
이 네 요소가 동시에 작용하며 “한 부분을 고쳐도 바로 해결되지 않는” 착시를 만들었다.
6. 최종 해결 (Resolution)
| 조치 | 설명 | 이유 |
|---|
1. declare module 블록 제거 (vite.config.ts) | 타입 선언을 전용 .d.ts 파일로 이동 | 안정적 전역 타입 공급 |
2. src/types/svg.d.ts 생성 | 네임드 + URL 동시 선언 | TS 컴파일러에 정확한 구조 명시 |
3. 플러그인 순서 정리: svgr() → react() | SVG → JSX → React 처리 정상화 | 변환 누락 방지 |
| 4. ICONS 생성 로직 개선 (ReactComponent | | default 함수) |
5. 캐시 삭제: rm -rf node_modules/.vite | 구 버전 transform 버퍼 제거 | 설정 반영 보장 |
| 6. 콘솔/typeof 진단 로그 도입 | 반복 진단 비용 감소 | 재발 시 즉시 원인 파악 |
7. 변경 전/후 주요 코드 비교
vite.config.ts (Before - 문제)
plugins: [
react(),
svgr({ svgrOptions: { exportType: 'named' }})
];
vite.config.ts (After - 정상)
import svgr from 'vite-plugin-svgr';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
svgr({
include: '**/*.svg',
svgrOptions: { exportType: 'named' },
}),
react(),
],
});
svg.d.ts (새로 추가)
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}
ICONS 맵 (Before - 축약)
if (mod.ReactComponent) ICONS[name] = mod.ReactComponent;
else console.warn('등록 불가');
ICONS 맵 (After)
const comp =
(typeof mod.ReactComponent === 'function' && mod.ReactComponent) ||
(typeof mod.default === 'function' && mod.default);
if (comp) ICONS[name] = comp;
8. 재발 방지 (Preventive Actions)
| 분류 | 액션 | 상태 |
|---|
| 문서화 | README 에 플러그인 순서/타입 선언 위치 규칙 추가 | 필요 |
| 자동화 | pre-commit 스크립트: vite.config.ts 내 declare module 패턴 탐지 | 고려 |
| 테스트 | 간단 smoke test: typeof ReactComponent === 'function' 검사 | 적용 가능 |
| 로깅 | ICONS 빌드 시 DEV 환경에서 keys 출력 | 적용 |
| 교육 | 온보딩 체크리스트에 “svg.d.ts 위치”, “캐시 삭제 단계” 추가 | 필요 |
| 옵션 | svgr 플러그인에 enforce: 'pre' 명시 | 권장 |
9. 빠른 진단 체커 (Runbook)
| 순서 | 질문 | 기대 | 아니면? |
|---|
| 1 | typeof ImportedIcon ? | function | string → 플러그인 순서/캐시 |
| 2 | hover 타입? | React.FC | string → svg.d.ts 또는 선언 오염 |
| 3 | Object.keys(mod) 에 ReactComponent? | 포함 | 미포함 → svgr 미작동 |
| 4 | ICONS keys 길이 > 0? | yes | 0 → 생성 로직/경로/패턴 |
| 5 | 캐시 삭제했나? | yes | no → .vite 삭제 후 재시도 |
| 6 | vite.config.ts 안에 declare module? | no | 제거 |
| 7 | 플러그인 순서? | svgr → react | 역순이면 교체 |
10. 최종 요약 (Executive TL;DR)
SVG 아이콘이 렌더링되지 않은 이유는
1) 전역 타입 선언 오용(vite.config.ts 내부)
2) svgr 플러그인 순서 오류
3) ICONS 빌드 로직의 불완전한 분기
4) 캐시가 변화 반영을 지연
이 복합적으로 쌓여 “단일 원인 추적”을 방해했기 때문이다.
표준 위치의 .d.ts + 올바른 플러그인 순서 + 포괄적 ICONS 로직 + 캐시 무효화로 해결되었고, 재발 방지용 체크리스트/문서화가 필요하다.
11. 추가 개선 아이디어 (Future Enhancements)
| 아이디어 | 기대 효과 |
|---|
아이콘 Lazy Load (import.meta.glob with dynamic import) | 초기 번들 크기 감소 |
| Wrapper 컴포넌트 (size/color 통일) | 스타일 일관성 |
| 빌드 전 아이콘 validate 스크립트 | 잘못된 SVG(속성/네임스페이스) 사전 차단 |
| Storybook 자동 목록 | 디자이너/개발 협업 효율 |
12. Action Items (Next Steps)
13. 교훈 (Lessons Learned)
- “한 번 고친 설정이 바로 안 먹는다” = 캐시 or 다중 원인 신호.
- 타입 선언은 항상 전용
.d.ts 로; 실행 config 파일과 혼합 금지.
- 플러그인 순서가 결과물의 데이터 “형” 자체를 바꿀 수 있다.
- 진단 로그( keys, typeof )는 비용이 거의 없는데, 탐색 시간을 크게 절감한다.
- 문제를 분리(단일 아이콘 → 자동 맵 → 전체 빌드)해 단계별로 축소하면 복합 오류도 빠르게 수렴 가능.