iOS Safari에서만 SVG 아이콘이 사라졌던 디버깅 기록
2025-12-29상품 카드 리스트에서 Pick 아이콘과 별점 아이콘을 SVG로 쓰고 있었는데, iOS에서만 아주 이상한 현상이 발생했어요. 첫 번째 카드의 아이콘은 잘 보이는데, 두 번째 카드부터는 동일한 아이콘이 통째로 보이지 않는 거였죠.
이 글은 그 트러블슈팅 과정과, 최종적으로 왜 clipPath를 제거하는 게 해답이 됐는지 정리한 내용이에요.
문제 상황
증상
상품 리스트 페이지에서 이런 현상이 발생했어요.
- 첫 페이지 진입 시 모든 아이콘은 정상 노출
- 하지만 다른 페이지로 이동했다가 다시 돌아왔을 때 첫 번째 카드 컴포넌트를 제외하고 다른 컴포넌트의 SVG 아이콘이 노출되지 않았어요.
개발자 도구로 확인한 결과
Chrome DevTools로 DOM과 스타일을 확인했을 때 이랬어요.
<svg>요소는 DOM에 존재해요width,height,display,opacity,fill모두 정상이에요- 첫 번째 카드와 나머지 카드의 CSS 차이가 없어요
- 하지만 화면에는 보이지 않아요
재현 환경
- iOS Safari (15+)
- iOS WebView
- Desktop Chrome (재현 안 됨)
- Desktop Safari (재현 안 됨)
- Android Chrome (재현 안 됨)
문제의 SVG 구조
문제가 됐던 StarIcon의 원본 SVG 코드예요.
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3200_4527)">
<path d="..." fill="#ECEEEF"/>
</g>
<defs>
<clipPath id="clip0_3200_4527">
<rect width="11" height="11" fill="white" transform="translate(0.183594)"/>
</clipPath>
</defs>
</svg>
원인
문제의 원인은 생각보다 간단했어요. SVG의 구성에 문제가 있었죠.
바로 <clipPath id="clip0_3200_4527"> 때문이었어요.
결과부터 얘기하면, SVG 컴포넌트를 카드마다 반복해서 렌더링하면, DOM 안에 동일한 id="clip0_3200_4527"를 가진 <clipPath>가 여러 번 등장하게 되고, 이게 아이콘이 사라지는 원인이에요.
SVG의 id 참조 메커니즘
React에서 배열을 렌더링할 때 저희는 항상 key를 지정해주잖아요? 이 key는 유니크해야 하고, 그렇지 않으면 UI상 문제가 발생하죠.
SVG도 똑같이 id는 문서 전체에서 유일해야 해요.
하지만 문제가 된 컴포넌트에서는 SVG 파일을 여러 번 inline으로 쓰는 경우가 많고, 이때 자연스럽게 id가 중복된 상태가 돼버려요.
// 이렇게 사용하면
{products.map(product => (
<ProductCard key={product.id}>
<StarIcon /> {/* 각각 동일한 id를 가진 clipPath 포함 */}
</ProductCard>
))}
// DOM에는 이렇게 됩니다
<svg>
<defs><clipPath id="clip0_3200_4527">...</clipPath></defs>
...
</svg>
<svg>
<defs><clipPath id="clip0_3200_4527">...</clipPath></defs> <!-- 중복! -->
...
</svg>
<svg>
<defs><clipPath id="clip0_3200_4527">...</clipPath></defs> <!-- 중복! -->
...
</svg>
브라우저별 처리 차이
대부분의 브라우저는 이 상황을 어느 정도 관대하게 처리해서 시각적으로 문제가 잘 드러나지 않아요.
하지만 WebKit(Safari 엔진)은 SVG 내부 id를 전역 단위로 처리하는 경향이 있어요.
그래서 DOM 복원 시 참조를 제대로 이어붙이지 못하는 버그가 있고, 특히 BFCache 복원 과정에서 문제가 자주 발생해요.
BFCache란?
BFCache는 브라우저가 페이지를 메모리에 저장해두고, 뒤로 가기/앞으로 가기 시 빠르게 복원하는 기능이에요.
iOS Safari는 이 기능을 적극적으로 쓰는데, 페이지를 완전히 새로 렌더링하는 게 아니라 스냅샷을 복원해요.
문제가 발생하는 과정
1. 첫 진입 시
각 카드의 SVG 컴포넌트가 렌더링됨
→ 각 <svg> 안의 <defs>, <clipPath>가 DOM에 붙음
→ clip-path="url(#clip0_3200_4527)"가 적절한 <clipPath>를 찾아감
→ 정상 렌더링됨
2. 다른 페이지로 이동
현재 페이지가 BFCache에 스냅샷으로 저장됨
→ DOM 상태가 메모리에 보존됨
3. 뒤로 가기로 돌아올 때
BFCache에 있던 DOM이 복원됨
→ WebKit이 inline SVG의 <defs>와 id 참조를 완벽하게 복원하지 못함
→ 동일한 id를 가진 <clipPath>가 여러 개 있을 때
- 어떤 <clipPath>에 매칭할지 애매해짐
- 첫 번째 인스턴스는 정상 작동
- 나머지는 참조가 끊기거나 잘못된 clipPath를 참조
→ 결과적으로 렌더링이 화면 밖으로 잘려나감
이런 문제 때문에 다시 돌아온 페이지에선 SVG 아이콘이 렌더링되지 않았던 거죠.
시도했던 해결책들
1차 시도: SVGR + SVGO 설정
처음엔 GPT 도움을 받아서 빌드 타임에 id 충돌을 줄이려고 SVGR 설정을 조정했어요.
// next.config.js
webpack(config, { isServer }) {
config.module.rules.push({
test: /\.svg$/i,
issuer: { and: [/\.(js|ts)x?$/] },
use: [
{
loader: "@svgr/webpack",
options: {
svgo: true,
svgoConfig: {
plugins: [
"preset-default",
"prefixIds", // id에 prefix를 붙여 충돌 방지
],
},
},
},
],
});
if (!isServer) {
config.resolve.fallback = {
fs: false,
dns: false,
net: false,
tls: false,
http2: false,
dgram: false,
};
}
return config;
}
하지만 위 방법을 써도 iOS에서 뒤로 가기 이후 SVG가 사라지는 문제는 그대로 재현됐어요.
id를 변형하거나 prefix를 붙이는 것만으로는 iOS Safari의 BFCache 복원 버그를 피할 수 없었던 거예요.
최종 해결: clipPath 제거
근본적인 원인을 해결하려고 SVG 구조를 다시 분석했어요.
viewBox="0 0 12 11"
clipPath: <rect width="11" height="11" transform="translate(0.183594)"/>
path: 대부분 viewBox 내부에 위치
해당 아이콘을 자세히 살펴보면 clipPath는 사실상 유의미한 역할을 하지 않았어요.
path 좌표가 이미 viewBox 내부에 있었고, 일부 음수 좌표가 있지만 화면 밖이라 제거해도 보이는 변화가 없었거든요. 또한 clipPath를 없애도 디자인이 변하지 않아요.
그래서 단순하지만 간단하게 문제가 되는 clipPath를 아예 삭제하는 방법을 썼어요.
AS-IS
<svg width="12" height="11" viewBox="0 0 12 11" fill="none">
<g clip-path="url(#clip0_3200_4527)">
<path d="..." fill="#ECEEEF"/>
</g>
<defs>
<clipPath id="clip0_3200_4527">
<rect width="11" height="11" fill="white" transform="translate(0.183594)"/>
</clipPath>
</defs>
</svg>
TO-BE
<svg width="12" height="11" viewBox="0 0 12 11" fill="none">
<path
d="..."
fill="#ECEEEF"
/>
</svg>
적용 후 모든 환경에서 문제가 완전히 해결됐어요.
| 환경 | 첫 진입 | 뒤로 가기 후 |
|---|---|---|
| iOS Safari | 정상 | 정상 |
| iOS WebView | 정상 | 정상 |
| Desktop Chrome | 정상 | 정상 |
| Desktop Safari | 정상 | 정상 |
| Android Chrome | 정상 | 정상 |
Inline SVG에서 id를 안전하게 관리하는 방법
이번 케이스처럼 clipPath를 아예 제거하는 게 가능한 경우도 있지만, 아이콘 디자인상 clipPath나 filter, mask가 반드시 필요한 경우도 있어요. 그럴 때 id 중복 문제를 다루는 방법들이에요.
1. 컴포넌트마다 고유 id 생성하기
가장 명확한 방법은 컴포넌트가 마운트될 때 고유한 id를 만들어 쓰는 것이에요.
import { useId } from 'react'; // React 18+
function StarIcon() {
const id = useId();
const clipId = `clip-${id}`;
return (
<svg width="12" height="11" viewBox="0 0 12 11" fill="none">
<g clipPath={`url(#${clipId})`}>
<path d="..." fill="#ECEEEF" />
</g>
<defs>
<clipPath id={clipId}>
<rect width="11" height="11" fill="white" transform="translate(0.183594)" />
</clipPath>
</defs>
</svg>
);
}
React 18에 추가된 useId는 서버/클라이언트 양쪽에서 안정적으로 고유한 id를 생성해요. 이전에는 Math.random()이나 Date.now() 기반 id를 쓰기도 했는데, 이는 SSR hydration 오류를 유발할 수 있어서 useId가 훨씬 안전한 선택이에요.
2. SVG sprite 방식으로 분리하기
페이지 어딘가에 SVG 정의를 한 번만 넣고, 다른 곳에서는 <use href="#id" />로 참조하는 방식이에요.
<!-- 페이지 최상단에 한 번만 정의 -->
<svg style="display:none">
<defs>
<clipPath id="star-clip">
<rect width="11" height="11" transform="translate(0.183594)" />
</clipPath>
</defs>
<symbol id="star-icon" viewBox="0 0 12 11">
<g clip-path="url(#star-clip)">
<path d="..." />
</g>
</symbol>
</svg>
<!-- 사용 시 -->
<svg width="12" height="11">
<use href="#star-icon" />
</svg>
id가 문서 전체에 단 하나만 존재하니까 중복 문제 자체가 발생하지 않아요. 다만 Next.js 같은 SSR 환경에서 SVG 스프라이트를 어디에 주입할지 관리가 필요해요.
3. 빌드 타임 id prefix (SVGO)
빌드 과정에서 SVGO의 prefixIds 플러그인이 각 SVG 파일의 id에 고유 prefix를 붙이도록 설정할 수 있어요. 하지만 이번 케이스에서 확인했듯이, id가 달라지더라도 iOS Safari의 BFCache 복원 과정에서 생기는 참조 오류는 막을 수 없어요. 따라서 이 방법은 단순 중복 방지에는 효과적이지만, BFCache 관련 이슈에는 useId나 sprite 방식이 더 근본적인 해결책이에요.
마치며
처음엔 "iOS에서만 아이콘이 사라진다" 는 증상만 보고 원인을 전혀 짐작하지 못했어요. DevTools로 DOM과 스타일을 아무리 봐도 차이가 없었거든요.
결국 문제는 세 가지가 맞물려 있었어요. SVG id의 전역 스코프, iOS Safari의 BFCache 복원 방식, 그리고 id 중복 시 참조가 끊기는 WebKit 동작. 이 중 하나라도 달랐다면 아무 문제가 없었을 거예요.
Figma에서 export한 SVG를 React 컴포넌트로 만들어 리스트에서 반복 사용할 때는 id 중복이 생길 수 있다는 점, 그리고 iOS Safari에서 뒤로 가기 후 이상한 렌더링 문제가 생긴다면 BFCache와 SVG id를 함께 의심해볼 만해요.