react‐native‐svg 라이브러리 도입 - study-pals/frontend GitHub Wiki

라이브러리 도입 배경

앱 내에서 svg 이미지를 사용하기 위한 설정이 필요했다. 관련 정보를 수집하였고, 조사 결과 react-native-svg가 가장 신뢰도 있는 라이브러리인 듯했다.

React가 <svg>를 처리하는 방법(Web 환경)

리액트는 <svg> 태그를 만났을 때, document.createElementNS(...)를 내부적으로 호출하여 SVG 네임스페이스로 요소를 생성한다.

이전 프로젝트(언제만나)에서는 svg를 처리하기 위해 외부 라이브러리를 사용하지 않고, svg 이미지를 파싱하여 리액트가 인식할 수 있는 형태로 바꾸어주는 transformer 스크립트를 직접 작성하여 svg 이미지를 전처리했었다.

아래는 그 커스텀 transformer 스크립트를 사용하여 svg 파일을 React가 처리할 수 있는 형태로 변환한 예시이다.

// Before: bare svg file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_732_173" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect width="24" height="24" fill="white"/>
</mask>
<g mask="url(#mask0_732_173)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.8585 19.9972C14.6948 19.9292 14.5461 19.8296 14.421 19.704L7.67101 12.954C7.54561 12.8286 7.44613 12.6798 7.37826 12.516C7.31039 12.3522 7.27545 12.1766 7.27545 11.9993C7.27545 11.8219 7.31039 11.6464 7.37826 11.4825C7.44613 11.3187 7.54561 11.1699 7.67101 11.0445L14.421 4.29451C14.5454 4.16478 14.6944 4.0612 14.8594 3.98983C15.0243 3.91846 15.2018 3.88074 15.3815 3.87889C15.5612 3.87703 15.7395 3.91108 15.9059 3.97903C16.0723 4.04698 16.2234 4.14747 16.3504 4.2746C16.4775 4.40173 16.5778 4.55294 16.6457 4.71937C16.7135 4.8858 16.7474 5.0641 16.7454 5.24381C16.7434 5.42351 16.7055 5.60101 16.634 5.7659C16.5625 5.93078 16.4588 6.07973 16.329 6.20401L10.5345 12L16.329 17.7945C16.5181 17.9832 16.6469 18.2238 16.6992 18.4858C16.7515 18.7477 16.7249 19.0193 16.6227 19.2662C16.5206 19.513 16.3475 19.724 16.1254 19.8724C15.9033 20.0209 15.6422 20.1001 15.375 20.1C15.1978 20.1001 15.0222 20.0652 14.8585 19.9972Z" fill="#8B95A1"/>
</g>
</svg>

// After: transformed as a react component (by our transformer script)
import type { IconProps } from '../Icon';

export const ArrowLeft = ({ clickable = false, className, width = 24, height = 24, fill = 'white', ...rest }: IconProps) => (
  <svg
    aria-label='arrow-left icon'
    className={className}
    fill='none'
    height={height || width}
    style={{ cursor: clickable ? 'pointer' : 'default', ...rest.style }}
    viewBox='0 0 24 24'
    width={width}
    xmlns='http://www.w3.org/2000/svg'
    {...rest}
  >
    <mask
      height='24'
      id='mask0_732_173'
      mask-type='luminance'
      maskUnits='userSpaceOnUse'
      width='24'
      x='0'
      y='0'
    >
      <rect
        fill='white'
        height='24'
        width='24'
      />
    </mask>
    <g mask='url(#mask0_732_173)'>
      <path
        clipRule='evenodd'
        d='M14.8585 19.9972C14.6948 19.9292 14.5461 19.8296 14.421 19.704L7.67101 12.954C7.54561 12.8286 7.44613 12.6798 7.37826 12.516C7.31039 12.3522 7.27545 12.1766 7.27545 11.9993C7.27545 11.8219 7.31039 11.6464 7.37826 11.4825C7.44613 11.3187 7.54561 11.1699 7.67101 11.0445L14.421 4.29451C14.5454 4.16478 14.6944 4.0612 14.8594 3.98983C15.0243 3.91846 15.2018 3.88074 15.3815 3.87889C15.5612 3.87703 15.7395 3.91108 15.9059 3.97903C16.0723 4.04698 16.2234 4.14747 16.3504 4.2746C16.4775 4.40173 16.5778 4.55294 16.6457 4.71937C16.7135 4.8858 16.7474 5.0641 16.7454 5.24381C16.7434 5.42351 16.7055 5.60101 16.634 5.7659C16.5625 5.93078 16.4588 6.07973 16.329 6.20401L10.5345 12L16.329 17.7945C16.5181 17.9832 16.6469 18.2238 16.6992 18.4858C16.7515 18.7477 16.7249 19.0193 16.6227 19.2662C16.5206 19.513 16.3475 19.724 16.1254 19.8724C15.9033 20.0209 15.6422 20.1001 15.375 20.1C15.1978 20.1001 15.0222 20.0652 14.8585 19.9972Z'
        fill='#8B95A1'
        fillRule='evenodd'
      />
    </g>
  </svg>
);

ArrowLeft.displayName = 'ArrowLeft';

react-native-svg의 역할

순수 React는 웹 브라우저 환경(DOM) 아래에서 동작한다는 전제를 가지므로, 내부적으로 DOM API를 호출하는 방식으로 svg를 브라우저에 렌더링한다. 하지만 React-Native는 브라우저 환경에서 실행되지 않으므로 다른 방법이 필요하다.

React-Native에서 Metro 번들러는 기본적으로 svg를 정적 에셋으로 간주한다. 따라서 svg를 렌더링하기 위해 개발자는 아래와 같이 <Image> 컴포넌트에 svg path를 uri로 제공하는 방식으로 작성해야 한다.

import someIcon from './someIcon.svg';

<Image source={someIcon} style={{ width: 24, height: 24 }} />

하지만 이 방식은 다음과 같은 한계가 존재한다.

  1. svg 내부 속성(color, size, etc.)을 JS를 통해 편집할 수 없다.
  2. <Image> 컴포넌트가 네이티브 이미지 라이브러리로 비트맵을 디코딩해 그리므로, 복잡한 svg나 svg 기반의 애니메이션을 렌더링할 때 오버헤드가 크다.

반면 react-native-svg는 네이티브 쪽에 <RNSVGSvg>, <RNSVGPath> 같은 원시 컴포넌트를 등록한 뒤, JSX에서 <Svg><Path> 같은 라이브러리 자체 컴포넌트를 호출하면 JSI/Bridge를 통해 해당 네이티브 컴포넌트를 렌더링하도록 연결한다.

위 방식을 통해 본래의 한계를 다음과 같이 극복할 수 있다.

  1. 자체 컴포넌트(<Svg>, <Circle>, <Path>)를 통해 svg를 처리함으로써 개발자가 JS를 통해 svg 속성을 변경할 수 있게 한다.
  2. 런타임에 벡터 그래픽을 직접 그리고, Animated API와 결합한 SVG 애니메이션 또한 지원한다.

상술한 내용은 react-native-svg 도입에 충분한 근거가 된다고 생각하였다. 라이브러리를 설치하기 위해서는 다음의 단계를 따른다.

npm install react-native-svg

# ios, macos
cd ios (or macos)
pod install

react-native-svg-transformer

react-native-svg를 사용하기 위해서는 기존의 raw svg 파일을 react-native-svg가 처리할 수 있는 형태로 재작성해주어야 한다. react-native-svg-transformer는 이를 자동화해주는 라이브러리로, 이전 우리 프로젝트에서 사용한 자체 transformer 스크립트와 비슷한 역할을 한다고 이해하면 된다.

react-native-svg-transformer는 svg 파일에 대한 변환만을 수행하므로, dev dependency로 설치한다.

npm install -D react-native-svg-transformer

이후 metro가 번들링 시 svg를 정적 에셋 취급하여 단순히 <Image>로 렌더링되는 것을 막기 위해, metro.config에 아래와 같은 설정을 추가한다.

const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");

const defaultConfig = getDefaultConfig(__dirname);
const { assetExts, sourceExts } = defaultConfig.resolver;

/**
 * Metro configuration
 * https://reactnative.dev/docs/metro
 *
 * @type {import('metro-config').MetroConfig}
 */
const config = {
  transformer: {
    babelTransformerPath: require.resolve(
      "react-native-svg-transformer/react-native"
    )
  },
  resolver: {
    assetExts: assetExts.filter((ext) => ext !== "svg"),
    sourceExts: [...sourceExts, "svg"]
  }
};

module.exports = mergeConfig(defaultConfig, config);

TypeScript를 위한 설정 추가

TypeScript는 기본적으로 .ts, .tsx, .js, .json만 알고, 그 외 확장자는 unknown module로 취급한다. 따라서 .svg를 모듈로 인식시키려면 ambient module declaration으로 확장자를 등록해 주어야 한다. 이를 위해 프로젝트에 declarations.d.ts 파일을 생성하고, 아래 코드를 작성한다.

declare module "*.svg" {
  import React from "react";
  import { SvgProps } from "react-native-svg";
  const content: React.FC<SvgProps>;
  export default content;
}
⚠️ **GitHub.com Fallback** ⚠️