iOS Safari에서만 SVG 아이콘이 사라졌던 디버깅 기록
상품 카드 리스트에서 Pick 아이콘과 별점 아이콘을 SVG로 쓰고 있었습니다. 그런데 iOS에서만 아주 이상한 현상이 보고됐습니다. 첫 번째 카드의 아이콘은 멀쩡히 보이는데, 두 번째 카드부터는 똑같은 아이콘이 통째로 사라지는 겁니다.
이 글은 그 트러블슈팅 과정과, 최종적으로 왜 clipPath를 제거하는 게 해답이 됐는지 정리한 기록입니다.
첫 번째 카드만 살아남았다
상품 리스트 페이지에서 벌어진 일은 이랬습니다.
- 첫 페이지 진입 시 모든 아이콘은 정상 노출
- 하지만 다른 페이지로 이동했다가 다시 돌아왔을 때, 첫 번째 카드 컴포넌트를 제외한 나머지 컴포넌트의 SVG 아이콘이 노출되지 않았습니다.
DevTools를 아무리 봐도 차이가 없었다
Chrome DevTools로 DOM과 스타일을 하나씩 뜯어봤습니다.
<svg>요소는 DOM에 그대로 존재합니다width,height,display,opacity,fill모두 정상입니다- 첫 번째 카드와 나머지 카드의 CSS 차이가 없습니다
- 그런데 화면에는 보이지 않습니다
재현 환경
- iOS Safari (15+)
- iOS WebView
- Desktop Chrome (재현 안 됨)
- Desktop Safari (재현 안 됨)
- Android Chrome (재현 안 됨)
iOS에서만, 그것도 뒤로 가기 이후에만 터진다는 뜻이었습니다.
문제의 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>
범인은 id 하나였다
원인은 생각보다 간단했습니다. SVG의 구성 자체에 문제가 있었죠.
바로 <clipPath id="clip0_3200_4527">였습니다.
결과부터 얘기하면, 이 SVG 컴포넌트를 카드마다 반복해서 렌더링하면 DOM 안에 동일한 id="clip0_3200_4527"를 가진 <clipPath>가 여러 번 등장하게 됩니다. 그게 아이콘이 사라진 원인이었습니다.
SVG의 id도 key처럼 유일해야 한다
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에서 뒤로 가기를 하니 아이콘은 또 사라졌습니다. 그대로 재현이었습니다.
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 | 정상 | 정상 |
clipPath를 지울 수 없다면
이번 케이스처럼 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를 함께 의심해보시길 권합니다.