본문 바로가기

프론트엔드/React

SVGR id collision issue 해결하기

  • 프로젝트: loplat UI
  • 키워드: svgr, svg, id collision, unique id, sed, regex
  • 상황
    1. loplat UI에서 svgr을 이용하여 Icon 컴포넌트를 만든다.
    2. 실제 프로젝트에서 Icon을 사용할 때, 하나의 아이콘을 2번 렌더링하면 이미지가 깨지는 현상을 발견했다.
       // 2번 렌더링
      <InfocircleOutlineIcon {...props} />
      ...
      <InfocircleOutlineIcon {...props} />​

      이미지가 깨진다
    3. 원인이 무엇인지 고민해보니 svg 속성 중 id가 random값이 아닌 고정값인 것을 확인했다. 
      __a, __b 가 suffix로 붙지만 'infocircle_outline_svg__a' 라는 id값 자체는 고정이라 컴포넌트를 2번 렌더링하면 id 충돌이 일어나는 것이다.
      __a, __b 로 suffix가 고정되어 있다
    4. loplat UI 코드를 통해 svgr 이 만들어주는 id는 static하는 것을 확인했다.
          <svg
          width={size}
          height={size}
          xmlns="http://www.w3.org/2000/svg"
          xmlnsXlink="http://www.w3.org/1999/xlink"
          style={style}
          className={className}
          viewBox="0 0 32 32"
        >
          <defs>
            <path
              d="..."
              id="infocircle_outline_svg__a" // id 고정
            />
          </defs>
          <g transform="translate(2 2)" fill="none" fillRule="evenodd">
            <mask id="infocircle_outline_svg__b" fill="#fff"> // id 고정
              <use xlinkHref="#infocircle_outline_svg__a" /> // id 고정
            </mask>
            <g mask="url(#infocircle_outline_svg__b)"> // id 고정
              <path fill={fillColor} d="M-2-2h32v32H-2z" />
            </g>
          </g>
        </svg>
    5. svgr에서 React Component를 만드는 template 파일을 확인해보았다.
        function defaultTemplate({ template }, _, { componentName, jsx }) {
        const typeScriptTpl = template.smart({ plugins: ['jsx', 'typescript'] });
        const IconComponentName = componentName.name.slice(3);
      
        return typeScriptTpl.ast`
          import React from 'react';
          import type { IconProps } from '../index';
          
          export const ${IconComponentName} = React.memo<IconProps>(({size = 18, fillColor = '#9DAAB7', className, style}) => {
            return ${jsx}
          })
        `;
      }
      
      module.exports = defaultTemplate;​

      'jsx' 부분에 svgr에서 만들어준 코드가 들어가는데, jsx 부분에 수정을 가하고 싶어도 불가능하다.(replace 등의 String prototype function을 적용할 수 없다.)

    6. svgr이 만들어주는 React Component 내용에 어떻게든 변형을 가하여, id 속성을 unique 하도록 개선해야한다.
      (svgr 라이브러리 자체에서 이 문제에 대한 해결법을 제공해주지는 않는다: https://github.com/gregberge/svgr/issues/150​)
  • 해결과정
    1. 일단 컴포넌트가 렌더링 될때마다 uniqueId가 생성되어야하기 때문에, 컴포넌트 내부에서 uniqueId를 자체적으로 생성해야한다. uniqueId를 generate하는 코드를 svgr template에 추가했다.
        export const ${IconComponentName} = React.memo<IconProps>(({size = 18, fillColor = '#9DAAB7', className, style}) => {
        const uniqueId = useMemo(() => String(Math.random().toString(36).substr(2, 9)), []);
        return ${jsx}
      })​

      아래 코드로 랜덤한 문자열을 만들었다.
        Math.random().toString(36).substr(2, 9)
    2. 그리고 @svgr/cli로 template을 실행시키면, 다음과 같은 컴포넌트가 만들어진다.
      const uniqueId = useMemo(() => String(Math.random().toString(36).substr(2, 9)), []);
      return (
        <svg ...>
          <defs>
            <path
              d="..."
              id="infocircle_outline_svg__a"
            />
          </defs>
          ...
      	<mask id="infocircle_outline_svg__b" fill="#fff">
      	  <use xlinkHref="#infocircle_outline_svg__a" />
      	</mask>
      	<g mask="url(#infocircle_outline_svg__b)">
      	  <path fill={fillColor} d="M-2-2h32v32H-2z" />
      	</g>
          ...
        </svg>
      );
      uniqueId 코드가 잘 생성되었지만, id 속성에서 전달할 수는 없는 상황이다.
      svgr template 파일에서 조작이 가능하다면 좋겠지만, 상황 5에 적어두었듯 다른 방법이 필요했다.
    3. file을 조작할 수 있는 shell script를 작성하기로 하고 file 수정이 가능한 'sed' 명령어를 발견했다.
       // assign-id-to-svg.sh
      grep -rl 'uniqueId' ../generated/*.tsx | xargs sed -i '' -e 's/id="\(.*\)" /id={`\1__${uniqueId}`} /'
      grep -rl 'uniqueId' ../generated/*.tsx | xargs sed -i '' -e 's/id="\(.*\)"/id={`\1__${uniqueId}`}/'
      grep -rl 'uniqueId' ../generated/*.tsx | xargs sed -i '' -e 's/xlinkHref="\(.*\)"/xlinkHref={`\1__${uniqueId}`}/'
      grep -rl 'uniqueId' ../generated/*.tsx | xargs sed -i '' -e 's/mask="url(#\(.*\))"/mask={`url(#\1__${uniqueId})`}/'​
       먼저 grep을 이용하여 조작할 파일을 지정한다. ('uniqueId'라는 문자열을 포함하고 있고 'generated' 폴더 안에 있는 txs 파일, 즉 Icon 컴포넌트 파일들)
      그 후, xargs를 이용하여 이 파일들을 sed 명령어의 인자로 전달한다.
      -i 옵션을 통해 파일을 '수정'하고, -e 옵션 뒤에는 실행할 명령을 적는다.
      실행문은 정규표현식을 이용하여 기존의 'id__a'를 'id__a__{uniqueId}' 형태로 치환한다.
      (정규표현식과 shell 스크립트를 다듬어 한 줄로 표현할 수도 있지만 가독성과 이해를 위해 4줄로 분리하였다)

    4. 그 뒤 yarn svgr 명령어에 해당 shell script를 실행하는 명령을 추가한다.(svgr cli와 template을 통해 리액트 컴포넌트를 만들고나서(해결과정 2), 그 파일들에 대해 assign-id-to-svg.sh 스크립트를 실행해야 함)
        // package.json
      "svgr": "npx @svgr/cli --template svgr-cli.template.js && ... && cd src/assets/Icon/tools && sh assign-id-to-svg.sh"
    5. 스크립트까지 적용한 Icon 컴포넌트의 모습이다.
        const uniqueId = useMemo(() => String(Math.random().toString(36).substr(2, 9)), []);
      return (
        <svg ...>
          <defs>
            <path
              d="..."
              id={`infocircle_outline_svg__a__${uniqueId}`} // id 랜덤
            />
          </defs>
          <g transform="translate(2 2)" fill="none" fillRule="evenodd">
            <mask id={`infocircle_outline_svg__b__${uniqueId}`} fill="#fff"> // id 랜덤
              <use xlinkHref={`#infocircle_outline_svg__a__${uniqueId}`} /> // id 랜덤
            </mask>
            <g mask={`url(#infocircle_outline_svg__b__${uniqueId})`}> // id 랜덤
              <path fill={fillColor} d="M-2-2h32v32H-2z" />
            </g>
          </g>
        </svg>
      );​


    6. 이제 하나의 Icon 컴포넌트를 2번 렌더링해도 id 충돌이 일어나지 않아 정상적으로 보인다!
      InfocircleOutline Icon