그라디언트가 문제? SVG 렌더링 오류 파헤치기

Jseok·2026년 4월 20일
post-thumbnail

개발을 진행하다보면, 때로 원인을 찾기 힘든 오류를 만나게 됩니다.

최근 개발을 진행하며 마주했던, SVG 관련 오류와 그 문제를 해결하고자 시도하고 고민했던 시간들을 기록하고자 합니다.

갑자기 아이콘이 왜 사라질까?

문제 상황은 위와 같은 뷰에서 발생했습니다. 간단하게만 설명하면, 공연 정보를 안내하는 카드 컴포넌트가 리스트 형태로 나열된 형태입니다. 오른쪽에 그라데이션 깃발 버튼은 클릭하여 관람 예정 공연 등록 및 해제를 할 수 있는 토글 버튼입니다.

모바일 웹 또는 PWA 환경에서 테스트를 하던 중 처음 보는 오류를 발견했습니다. 관람 예정 탭에서 특정 공연 카드의 '관람 예정 해제 버튼'을 누르면, 리스트가 갱신되면서 엉뚱하게도 아래에 위치한 다른 카드의 깃발 아이콘이 사라지는 현상이었습니다.

데스크탑 환경에서는 문제가 없어서 발견하지 못했었는데, 모바일 환경에서만 해당 오류가 발생했습니다.


리액트의 렌더링 동기화 문제일까?

가장 먼저 의심했던 것은 리액트의 리스트 렌더링 방식이었습니다.

리액트는 리스트 렌더링 시 key를 기준으로 컴포넌트의 동일성을 판단하고, 동일한 key를 가진 컴포넌트를 재사용하려 합니다. 따라서 keyindex인 경우 동일한 key를 가진 하위 컴포넌트를 재사용하려고 하기 때문에, 실제 데이터와 다른 컴포넌트가 매칭되어 예상치 못한 오류가 발생할 수 있습니다.

이를 현재 발생한 오류 상황에 대입해보면, 리스트 렌더링 과정에서 key가 적절하지 않을 경우 컴포넌트가 재사용되면서 예상치 못한 문제가 발생할 수 있다고 생각했습니다.

해결 시도

따라서 FlagButton 컴포넌트에 카드의 고유 값인 festivalIdkey로 할당하여, 노드를 재사용하지 않고 새로 생성하여 누락 없이 아이콘을 다시 그리도록 시도했습니다.

위 변경사항으로 오류가 해결되기를 기대했지만, SVG 아이콘이 사라지는 문제는 여전히 지속되었습니다. 이제 렌더링 방식은 원인이 아니라는 것을 확실히 알게 되었고, 다른 원인을 생각해봐야 했습니다.


그라디언트 SVG 자체가 원인은 아닐까?

위에서 문제를 해결하지 못하고 다른 방향으로 접근했기에, 원점으로 돌아가 처음부터 천천히 다시 생각했습니다.

그러던 중 한 가지 단서를 찾을 수 있었습니다. 일반 단색 SVG는 사라지지 않는데, 그라디언트가 적용된 SVG에서만 오류가 발생한다는 것이었습니다. 그러면 혹시 그라디언트 SVG 자체에 원인이 있을 수 있겠다는 생각이 들어, 내부 코드를 열어봤습니다.

<linearGradient id="paint0_linear_524_15277" x1="9.64982" y1="4.34995" x2="24.4999" y2="15" >
...
</linearGradient>

여기서 답을 찾을 수 있었습니다. 원인은 SVG 파일 내부의 id 속성에 있었습니다.

현재 모든 SVG를 svgr을 사용해 인라인 리액트 컴포넌트로 렌더링하고 있습니다.

  • 인라인으로 위 그라디언트 SVG를 여러 번 렌더링하면, DOM 트리에 paint0_linear_524_15277이라는 동일한 id를 가진 요소가 중복해서 생성됩니다.

  • 이 상황에 리스트에서 요소 하나가 삭제되면, 브라우저가 삭제된 컴포넌트에 포함되어 있던 linearGradient 정의도 함께 제거합니다. 남아있는 다른 아이콘들은 여전히 같은 id를 참조하고 있지만, 참조할 대상이 사라졌으므로 그라디언트 색상을 칠하지 못하고 투명하게 렌더링 되었다고 판단할 수 있습니다.

결국 문제의 핵심은 SVG의 id는 전체에서 공유되는 전역 값이기 때문에, 동일한 SVG를 여러 번 렌더링할 경우 예상치 못한 충돌이 발생할 수 있다는 것입니다.


그렇다면 어떻게 해결할 수 있을까

이제 핵심 원인으로 추측되는 요소를 찾았고, 해결 방법을 찾아야 했습니다.

크게 2가지 방법을 사용해볼 수 있습니다.

  • useId 활용 컴포넌트 생성 : 래퍼 컴포넌트를 따로 정의하여 useId로 매번 고유한 아이디를 생성해 주입하는 방법
  • img 태그로 격리 : svgr 방식을 포기하고 img 태그로 격리

📌 useId 활용 컴포넌트 생성

import { useId } from 'react';

const GradientFlagIcon = () => {
  const id = useId();

  // 각각 다른 gradient id 생성
  const gradient0 = `paint0-${id}`;
  const gradient1 = `paint1-${id}`;

  return (
    <svg
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d= // 생략
        fill={`url(#${gradient0})`}
      />
      <path
        d= // 생략
        fill={`url(#${gradient1})`}
      />

      <defs>
        <linearGradient
          id={gradient0}
          x1="9.64982"
          y1="4.34995"
          x2="24.4999"
          y2="15"
          gradientUnits="userSpaceOnUse"
        >
          <stop stopColor="#ACF6D6" />
          <stop offset="1" stopColor="#16C284" />
        </linearGradient>

        <linearGradient
          id={gradient1}
          x1="7.68927"
          y1="1"
          x2="9.68927"
          y2="23"
          gradientUnits="userSpaceOnUse"
        >
          <stop stopColor="#6EE7B7" />
          <stop offset="1" stopColor="#34D399" />
        </linearGradient>
      </defs>
    </svg>
  );
};

export default GradientFlagIcon;

첫 번째는 id 충돌을 방지하기 위해 svgr 방식을 사용하지 않고, 리액트의 useId 훅을 사용하여 래퍼 아이콘 컴포넌트를 따로 생성하는 것입니다.

렌더링마다 고유한 id를 생성하고, 해당 값을 <linearGradient>id<path>fill 속성에 동일하게 적용했습니다. 기존 svg 코드에서 id 할당 방식을 제외하고는 모두 동일합니다. 이렇게 하면 각 컴포넌트 인스턴스가 서로 다른 id를 가지게 되어 충돌 없이 안전하게 그라디언트를 사용할 수 있습니다.

하지만 이렇게 했을 때, 두 가지 단점도 존재합니다.

  • 아이콘 하나 때문에 따로 컴포넌트를 생성했으므로, 유지보수 포인트가 늘어납니다.

  • 런타임에 그라디언트 색상을 동적으로 바꿔야 하는 상황이 아니라면, 굳이 React 컴포넌트로 관리할 필요가 없습니다.

📌 img 태그로 격리

두 번째 방식은 SVG 아이콘을 img 태그 안에 넣어서 격리하는 방식입니다.

현재 그라디언트 깃발 아이콘은 런타임에 색상이나 형태를 동적으로 조작할 필요가 없는 정적 아이콘입니다. 따라서 간단하게는 img 태그를 사용하여 격리해도 되겠다고 생각했습니다. img 태그로 SVG를 불러오면 브라우저는 해당 리소스를 메인 DOM과 분리된 독립적인 문서로 취급합니다.

따라서 외부 DOM에 있는 id와 충돌이 일어나지 않아, 동일하게 오류를 해결할 수 있습니다.

💭 SVGO prefixIds는 안될까?

이번 오류를 해결하기 위해 찾아보던 중, SVGO의 prefixIds 플러그인을 발견했습니다.

이 플러그인도 id 충돌 방지를 위해 사용하기 때문에 언뜻 보면 이번 오류 또한 해결할 수 있을 것이라 생각했으나 실제로 시도해보니 이 상황에서는 통하지 않았습니다.

이유는 아래와 같습니다.

  • SVGO는 빌드 타임 도구입니다. prefixIds는 코드를 빌드할 때 파일명을 기반으로 id에 고정된 접두사를 붙여줍니다. 따라서 아이콘 간 id 중복을 막기 때문에, 서로 다른 SVG 파일 간의 충돌을 막는데에는 유용합니다.

  • 하지만 지금 우리의 상황은 런타임에 동일한 컴포넌트를 배열의 map을 돌며 여러 번 렌더링 하는 방식입니다. 빌드 타임에 아무리 접두사를 붙여도, 런타임에는 똑같은 접두사가 붙은 id가 여러 번 사용되므로 사실상 동일한 오류가 발생할 수 밖에 없습니다.


물론 더 나은 해결 방법이 있을 수 있습니다.

다만 이번 글은 해결 방법보다는, 원인을 찾아가는 과정에 더 큰 의미가 있다고 생각합니다. 생각하지 못했던 원인을 향해, 내가 생각했던 원인들을 하나씩 소거하며 다가갔다는 점에 의미가 있는 것 같습니다.

만약 비슷한 문제를 겪고 있다면, 이 글이 작은 힌트가 되어 조금이나마 도움이 되기를 바라는 마음입니다.

0개의 댓글