반응형
- 프로젝트: loplat UI
- 키워드: svgr, svg, id collision, unique id, sed, regex
- 상황
- loplat UI에서 svgr을 이용하여 Icon 컴포넌트를 만든다.
- 실제 프로젝트에서 Icon을 사용할 때, 하나의 아이콘을 2번 렌더링하면 이미지가 깨지는 현상을 발견했다.
// 2번 렌더링 <InfocircleOutlineIcon {...props} /> ... <InfocircleOutlineIcon {...props} />
- 원인이 무엇인지 고민해보니 svg 속성 중 id가 random값이 아닌 고정값인 것을 확인했다.
__a, __b 가 suffix로 붙지만 'infocircle_outline_svg__a' 라는 id값 자체는 고정이라 컴포넌트를 2번 렌더링하면 id 충돌이 일어나는 것이다.
- 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>
- 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을 적용할 수 없다.) - svgr이 만들어주는 React Component 내용에 어떻게든 변형을 가하여, id 속성을 unique 하도록 개선해야한다.
(svgr 라이브러리 자체에서 이 문제에 대한 해결법을 제공해주지는 않는다: https://github.com/gregberge/svgr/issues/150)
- 해결과정
- 일단 컴포넌트가 렌더링 될때마다 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)
- 그리고 @svgr/cli로 template을 실행시키면, 다음과 같은 컴포넌트가 만들어진다.
uniqueId 코드가 잘 생성되었지만, id 속성에서 전달할 수는 없는 상황이다.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> );
svgr template 파일에서 조작이 가능하다면 좋겠지만, 상황 5에 적어두었듯 다른 방법이 필요했다. - file을 조작할 수 있는 shell script를 작성하기로 하고 file 수정이 가능한 'sed' 명령어를 발견했다.
먼저 grep을 이용하여 조작할 파일을 지정한다. ('uniqueId'라는 문자열을 포함하고 있고 'generated' 폴더 안에 있는 txs 파일, 즉 Icon 컴포넌트 파일들)// 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})`}/'
그 후, xargs를 이용하여 이 파일들을 sed 명령어의 인자로 전달한다.
-i 옵션을 통해 파일을 '수정'하고, -e 옵션 뒤에는 실행할 명령을 적는다.
실행문은 정규표현식을 이용하여 기존의 'id__a'를 'id__a__{uniqueId}' 형태로 치환한다.
(정규표현식과 shell 스크립트를 다듬어 한 줄로 표현할 수도 있지만 가독성과 이해를 위해 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"
- 스크립트까지 적용한 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> );
- 이제 하나의 Icon 컴포넌트를 2번 렌더링해도 id 충돌이 일어나지 않아 정상적으로 보인다!
- 일단 컴포넌트가 렌더링 될때마다 uniqueId가 생성되어야하기 때문에, 컴포넌트 내부에서 uniqueId를 자체적으로 생성해야한다. uniqueId를 generate하는 코드를 svgr template에 추가했다.
반응형
'프론트엔드 > React' 카테고리의 다른 글
Toast 컴포넌트 구현하기 (1) | 2022.01.26 |
---|---|
리액트에서 달팽이 모양으로 움직이는 애니메이션 만들기 (0) | 2022.01.19 |
useLayoutEffect로 최초 렌더링 UX 개선하기 (0) | 2022.01.16 |
Firebase auth 정보를 redux로 관리하기 (0) | 2021.11.18 |
useSetRecoilState로 렌더링 최적화하기 (0) | 2021.11.15 |