아이콘 같은 간단한 이미지를 다룰 때는 PNG 형식보다는 SVG 가 더 좋다. 하지만 React Native 에서는 기본적으로 SVG 파일을 지원하지 않는다.

그럼 React Native 에서 SVG 파일을 아이콘으로 사용할 수 있도록 설정 및 구현해보자.

1. 환경 구성 및 설정

1.1. react-native-svg

react-native-svg 는 React Native 프로젝트에서 SVG 및 관련 엘리먼트들을 웹과 유사한 형식으로 사용할 수 있게 해주는 라이브러리다. 이 라이브러리를 먼저 설치하자.

yarn add react-native-svg
cd ./ios && pod install

설치만 하면 된다. 특별한 설정은 필요 없다.

1.2. react-native-svg-transformer

react-native-svg 는 <Svg />, <G />, <Path /> 등 거의 모든 SVG 관련 컴포넌트를 제공하지만, SVG 파일 자체를 import 할 수 있게 해주지는 않는다. SVG 파일들을 import 해서 사용하려면 react-native-svg-transformer 라이브러리도 필요하다. 이 라이브러리는 react-native-svg 라이브러리를 사용해 SVG 파일을 읽어들여 React 컴포넌트로 사용할 수 있게 해준다.

yarn add --dev react-native-svg-transformer

설치 이후 추가적인 설정이 필요하다. (react-native-svg-transformer 의 README에도 잘 나와있다.)

1.2.1. metro.config.js

프로젝트의 루트 디렉토리에 metro.config.js 파일이 있다. 기본 설정되어있는 내용과 라이브러리 README.md 의 metro.config.js 내용을 병합하자. 아래는 병합한 결과물 예시이다.

// metro.config.js

const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
  const {
    resolver: { sourceExts, assetExts },
  } = await getDefaultConfig();
  return {
    transformer: {
      getTransformOptions: async () => ({
        transform: {
          experimentalImportSupport: false,
          inlineRequires: true,
        },
      }),
      babelTransformerPath: require.resolve("react-native-svg-transformer"),
    },
    resolver: {
      assetExts: assetExts.filter((ext) => ext !== "svg"),
      sourceExts: [...sourceExts, "svg"],
    },
  };
})();

1.2.2. declaration.d.ts

타입스크립트를 사용한다면 프로젝트 루트 디렉토리에 declaration.d.ts 파일을 새로 만들어서 아래 내용을 넣어주어야 한다.

// declaration.d.ts

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

1.2.3. .svgrrc

이 파일은 꼭 작성할 필요는 없다. 하지만 작성하면 읽어들인 SVG 컴포넌트의 어트리뷰트를 동적으로 지정할 수 있게 된다. 프로젝트 루트 디렉토리에 .svgrrc 파일을 만들어 아래 내용을 넣자.

// .svgrrc
{
  "replaceAttrValues": {
    "#000": "{props.fill}"
  }
}

이렇게 설정하면 읽어들인 svg 파일 내에서 "#000"로 값이 설정된 어트리뷰트는 fill prop 의 값으로 치환된다.

<!-- Logo.svg -->
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M2.965 6.0925C4.045 8.215 ..." fill="#000"/>
</svg>

위처럼 작성된 Logo.svg 파일을 import 해서 아래처럼 쓸 수 있다.

import Logo from "./Logo.svg";

// ...
const SomeComp = () => (
  // 이렇게 하면 Logo.svg 파일 내의 "#000" 어트리뷰트를 "#FFF" 로 치환되어 적용된다.
  <Logo width={120} height={40} fill={"#FFF"} />
);

이 기능은 react-native-svg-transformer 가 내부적으로 SVGR이라는 라이브러리를 사용해서 구현했다. 따라서 SVGR 에서 지원하는 다른 설정들도 사용할 수 있다. 좀 더 정보를 얻고 싶다면 해당 라이브러리의 문서를 확인하자.

# .svgrrc 설정을 수정해도 적용되지 않는다?

.svgrrc 설정을 수정하고 다시 빌드해도 수정한 사항이 적용되지 않을 때가 있다. 그럴 때는 metro 를 끄고 yarn start --reset-cache 로 다시 실행시켜주자.

2. 공통 아이콘 컴포넌트 작성

이제 SVG 파일을 웹에서처럼 읽어들여 사용할 수 있다. 하지만 SVG 파일을 일일이 따로따로 import 해줘야 하므로 사용하기 번거롭고, 아이콘들의 공통 props 혹은 속성을 관리하기도 힘들다.

그러한 불편함을 해결하기 위해 별도의 컴포넌트를 하나 구현할 것이다. 해당 컴포넌트는 아이콘 이름만으로 아이콘을 사용할 수 있게 해줄 것이며 공통적으로 쓰일 props 들도 관리할 것이다.

2.1. SVG 파일들 re-export

일단 컴포넌트를 작성하기 전에, 모든 SVG 파일은 직접 import 해서 쓰는 게 아니라 인덱스 파일을 따로 만들어 한 곳에서 관리하도록 하자.

// src/res/index.ts
export { default as Calendar } from "./calendar.svg";
export { default as Clock } from "./clock.svg";
export { default as Sandwatch } from "./sandwatch.svg";
export { default as Watch } from "./watch.svg";

이제 다른 파일에서는 아래와 같은 형식으로 import 가 가능하다.

import { Calendar } from "../res";
<Calendar />;
// 혹은
import * as icons from "../res";
const Comp = icons["Calendar"];
<Comp />;

2.2. <SvgIcon /> 구현

하나의 파일에 묶이게 된 SVG 파일들을 읽어와 사용하는 컴포넌트 <SvgIcon /> 은 아래처럼 구현할 수 있다.

// src/components/SvgIcon.tsx
import React from "react";
import { SvgProps } from "react-native-svg";

import * as Icons from "../res";

type IconProps = SvgProps & {
  // res 에서 re-export 되는 SVG 파일들의 이름을 name 으로 받을 수 있다.
  name: keyof typeof Icons;
  size?: number;
};
function Icon({
  name,
  fill = "black",
  width: _width,
  height: _height,
  size,
  ...props
}: IconProps) {
  const Comp = Icons[name];
  // `width`, `height` 를 따로 지정할 수 있지만
  // 아이콘은 보통 가로 세로 값이 같은 정사각형 형식이기 때문에
  // 여기서는 `size` 를 사용해 너비와 높이를 같이 지정할 수 있게 해주었다.
  const width = _width ?? size;
  const height = _height ?? size;
  const sizeProps = {
    ...(width !== undefined ? { width } : {}),
    ...(height !== undefined ? { height } : {}),
  };

  return (
    <Comp
      {...props}
      // 1.2.3. `.svgrrc` 의 설정 덕분에 `fill` prop 을 이렇게 사용할 수 있다.
      fill={fill}
      {...sizeProps}
    />
  );
}

export default Icon;

3. 결과

3.1. 사용 예

구현한 컴포넌트 <SvgIcon />을 아래처럼 사용할 수 있다.

// ...
import SvgIcon from "./components/SvgIcon";

const App = () => {
  return (
    <SafeAreaView style={{ flex: 1, padding: 24 }}>
      {/* ... */}
      <SvgIcon name="Calendar" />
      <SvgIcon name="Clock" fill="orange" />
      <SvgIcon size={48} name="Watch" fill="gold" />
      {/* ... */}
    </SafeAreaView>
  );
};

3.2. 소스코드 전문

설정 및 구현이 적용된 실제 소스코드를 참고하고 싶다면 RnSvgIconExample 리파지토리를 참고하자. 이 문서를 작성하면서 다시 한 번 적용 및 구현해본 리파지토리이며, 실행 또한 잘 된다.