개발자
류준열
SVG 컴포넌트 개발기
SVG 아이콘을 사용할 때마다 매번 파일을 저장하고, 이름을 외우고, 직접 import해서 사용하는 과정이 꽤 번거롭게 느껴졌다. 그러던 중 쏘카 기술 블로그를 보고 영감을 받아, 우리 프로젝트에도 비슷한 방식을 도입해보게 되었다.
(dynamic import를 사용한 방식도 있는데, 이는 태생적으로 로딩이 발생할 수 밖에 없는 구조라 보고 넘겼다.)
기존 문제점
기존에는 SVG 파일을 assets 폴더에 저장한 뒤, 아래와 같이 직접 import해서 사용했다.
import Search from "@/../public/assets/icon/search.svg"; ... <InputWrapper> <Input /> <Icon component = {Search} /> </InputWrapper>
이 방식은 다음과 같은 불편함이 있었다.
- 직접 렌더링 시키기 전에는 어떻게 생겼는지 알 수 없음.
- 동일한 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`} // 피그마에 등록된 svg의 width, height가 24,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" />
이 방식은 아이콘 사용의 일관성, 유지보수성, 특히 가독성을 크게 개선해주었다. (렌더링하지 않아도 어떤 아이콘인지 알 수 있음)