개발자
류준열

SVG 컴포넌트 개발기

SVG 아이콘을 사용할 때마다 매번 파일을 저장하고, 이름을 외우고, 직접 import해서 사용하는 과정이 꽤 번거롭게 느껴졌다. 그러던 중 쏘카 기술 블로그를 보고 영감을 받아, 우리 프로젝트에도 비슷한 방식을 도입해보게 되었다.

(dynamic import를 사용한 방식도 있는데, 이는 태생적으로 로딩이 발생할 수 밖에 없는 구조라 보고 넘겼다.)

기존 문제점

기존에는 SVG 파일을 assets 폴더에 저장한 뒤, 아래와 같이 직접 import해서 사용했다.

import Search from "@/../public/assets/icon/search.svg";

...
<InputWrapper>
  <Input />
	<Icon component = {Search} />
</InputWrapper>

이 방식은 다음과 같은 불편함이 있었다.

  1. 직접 렌더링 시키기 전에는 어떻게 생겼는지 알 수 없음.
  2. 동일한 svg가 서로 다른 이름으로 중복 저장되는 경우가 있었다. (ex: search.svg, search_caution.svg)

개선 방향 및 설계 의도

우리 팀은 디자인 시스템에 등록된 아이콘만 사용하기로 정했고, 해당 아이콘들은 모두 Figma에 정의되어 있었다. 이에 따라 아이콘 사용을 아래와 같이 단순화하고자 했다

  • 아이콘 이름만 알면 사용 가능하도록 props 기반 접근 제공
  • 중복 저장 방지를 위해 아이콘 데이터를 코드로 중앙 관리

먼저, Figma에 등록된 아이콘 이름을 기반으로 한 ICONS 객체를 정의하고, 각 아이콘의 path 데이터를 문자열로 저장했다.

export const ICONS = {
  Setting:
    "M7.99204 ... 11.8746 5.59249 10.001Z",
  Info: "M12 8.72368C11.5858 ... 6.47715 22 12 22Z",
  Minus:
    "M2.25 12C2.25 11.5858 ...  11.5858 2.25 12 2.25Z",
  Close:
    "M4.46967 4.46967C4.76256 ... 4.17678 4.76256 4.46967 4.46967Z",
  Refresh:
    "M15.674 5.23438C13.04  ... 1.96683 8.44187 2.34775 8.27916L4.6468 7.29716Z",
  Download:
    "M12 3.25C12.4142 3.25 ... 14.7523 3.47654 14.3693 3.88594 14.3063Z",
  Upload:
    "M12 3.25C12.2052 3.25 ... 3.88594 14.3063Z",
  Delete:
    "M9.75 4.5C9.75 3.5335 ... 13.5858 8.75 14 8.75Z",
};

SVG의 path만 추출해서 저장한 이유는 다음과 같다

  • SVG 전체 태그를 보관하는 것보다 훨씬 경량화되며,
  • viewBox, fill, width, height 등의 속성을 런타임에서 커스터마이징할 수 있어 재사용성이 높아진다.

Icon 컴포넌트 구현

이제 아이콘을 이름 기반으로 불러올 수 있는 Icon 컴포넌트를 만들었다.

핵심 설계 포인트는 다음과 같다:

  • icon prop으로 문자열 이름을 받고, 해당 이름에 해당하는 path를 ICONS 객체에서 찾는다.
  • 필요에 따라 크기(size), 색상(color), 회전(rotate), 스핀(spin) 등의 속성을 함께 커스터마이징할 수 있다.
  • 디자인 시스템에서 지정한 기본 사이즈와 색상을 default로 지정해 일관성 유지
import * as React from "react";
import { ICONS } from "./constants";
export interface IconBaseProps extends Omit<React.HTMLProps<HTMLSpanElement>, "width" | "height"> {
  spin?: boolean;
  rotate?: number;
}

export type IconProps = keyof typeof ICONS;

export interface CustomIconComponentProps {
  size?: string | number;
  fill?: string;
  viewBox?: string;
  className?: string;
}

export interface IconComponentProps extends IconBaseProps {
  viewBox?: string;
  component?:
    | React.ComponentType<CustomIconComponentProps | React.SVGProps<SVGSVGElement>>
    | React.ForwardRefExoticComponent<CustomIconComponentProps>;
  ariaLabel?: React.AriaAttributes["aria-label"];
  color?: string;
  icon: IconProps; // 피그마에 디자이너가 작성한 svg이름을 props로 받는다.
}

// eslint-disable-next-line react/display-name
export const Icon = React.forwardRef<HTMLSpanElement, IconComponentProps>((props, ref) => {
  const { icon, size: propSize, spin, rotate, className, color: propColor, ...restProps } = props;
  const size = propSize || "24px";
  const fill = propColor || "var(--Grey-G-10)";
  const iconClassName = `${className || ""} ${spin ? "icon-spin" : ""}`;

  const path = ICONS[icon];

  return (
    <span ref={ref} className={iconClassName} {...restProps}>
      <svg
        width={size}
        height={size}
        viewBox={`0 0 24 24`} // 피그마에 등록된 svgwidth, height24,24        fill={fill}
        fillRule="evenodd"
        clipRule="evenodd"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path d={path} />
      </svg>
    </span>
  );
});

사용 예시

이제는 피그마에 등록된 이름만 알면 다음과 같이 간단히 사용할 수 있다.

<Icon icon = "Refresh" size = {32} color = "#2D8CFF" />

이 방식은 아이콘 사용의 일관성, 유지보수성, 특히 가독성을 크게 개선해주었다. (렌더링하지 않아도 어떤 아이콘인지 알 수 있음)

피그마에 작성된 svg 이름을 props로 받는 Icon 컴포넌트