svg icon 라이브러리 만들기

2024년 11월 5일

서론

sopt makersSOPT 동아리 운영에 필요한 프로덕트를 만듭니다. SOPT 동아리에서 사용하는 여러 프로덕트 간 일관된 디자인과 효율적인 개발을 위해서 디자인 시스템의 필요성이 제기되었습니다. 이에 공감하여, 디자인 시스템 TF에 참여하게 되었습니다.

디자인 시스템은 모든 개발자가 편리하게 사용할 수 있어야 한다고 생각했기 때문에, 혼자서만 고민하고 개발하기보다 여러 개발자의 의견을 듣고 최적의 사용 방법을 찾아갔습니다.

icon 라이브러리를 개발하며 마주쳤던 고민들과 그 해결 과정을 정리해 보았습니다.

본론

1. icon 네이밍 규칙과 폴더 구조

수십 개의 아이콘을 효율적으로 분류할 기준이 필요했습니다. 이를 위해, 디자이너분들이 Figma에서 각 아이콘에 지정한 이름 앞에 Icon이라는 prefix를 붙이기로 했습니다. 이렇게 함으로써, 개발자가 Figma에 명시된 아이콘 이름만 보고도 원하는 아이콘을 쉽게 찾을 수 있도록 했습니다.

아이콘은 지정된 그룹에 맞추어 폴더별로 정리했습니다. 각 폴더에 존재하는 index.ts 파일은 폴더 내 모든 .tsx 파일을 export하도록 구성하였고, 최상위 index.ts에서는 각 폴더의 index를 export하여 전체 폴더 구조를 잡았습니다.

├─ src
 ├─ Icon
  ├─ Communication // icon 종류에 따라 폴더 구분
   ├─ ic-archive.tsx
   ├─ ic-bookmark.tsx
   ├─ ic-edit.tsx
   └─ index.ts // 각 폴더별 icon export
...
  ├─ icon.d.ts
  └─ index.ts // 전체 icon export

src/Icons/{폴더명}/index.ts에서 각 폴더 내부에 있는 모든 컴포넌트들을 export 합니다.

// src/Icons/{폴더명}/index.ts

export { default as IconArchive } from "./ic-archive";
export { default as IconBookMark } from "./ic-bookmark";
export { default as IconEdit } from "./ic-edit";

src/Icons/index.ts에서 폴더별로 전체 icon 코드 export 합니다.

// src/Icons/index.ts

export * from "./src/Communication/index";
export * from "./src/Editor/index";
export * from "./src/Files/index";
export * from "./src/Interaction/index";
export * from "./src/Media/index";
export * from "./src/Users/index";
...

2. jsx component로 svg 사용하기

모든 sopt-makers 프로덕트에서 next.js를 사용하고 있어 아래 예시처럼 사용자가 icon.svg을 import할 때 react component로 변환하여 사용하는 방식을 생각했습니다.

import { ReactComponent as IconArchive } from "@sopt-makers/icons";

하지만 이 방식은 프레임워크/라이브러리, 더 나아가 번들러 종류에 따라 제한적으로 사용될 수 있습니다. 각 환경에서 SVG 파일을 처리하는 방법이 다를 수 있기 때문입니다.

svg 파일을 import 하는 방식은 webpack, esbuild, babel 등의 번들러를 활용해 svg 파일을 인라인 자바스크립트 코드로 변환합니다. 대표적으로 Next.js 에서는 svg import 를 지원하기 위해서 svgr 이라는 웹팩 로더를 사용합니다. 다음과 같이 webpack config에 svgr 로더를 넣어주면, build 과정에서 .svg 를 import 하는 부분을 읽고 해당 svg를 인라인으로 변환한 컴포넌트 파일을 생성하여 import하는 방식으로 바꿔주는 동작합니다.

// config

const nextConfig = {
  reactStrictMode: true,
  webpack: (config) => {
    config.module.rules.push({
      test: /\.svg$/,
      use: ["@svgr/webpack"],
    });
    return config;
  },
};

export default nextConfig;

따라서 번들러와 세팅 방식의 구현 환경에 따라 import한 svg의 동작 방식이 달라질 위험이 있었습니다.

svg를 jsx 형태로 tsx 파일에 담아 export하는 방식으로 구현했습니다.

3. forwardRef

동아리 내 여러 프로젝트에서 Framer Motion 등을 사용하여, ref를 통해 svg에 직접 접근해야하는 상황이 있었습니다. 이를 위해 forwardRef를 활용해 외부로부터 ref를 전달받을 수 있도록 하였습니다. 또한, svg 스타일 속성을 CSS로 제어하는 props도 받아서 사용할 수 있도록 다음과 같이 tsx 파일을 작성했습니다.

import { HTMLAttributes, forwardRef } from 'react';

interface IconArchiveProps extends HTMLAttributes<SVGSVGElement> {
}

const IconArchive= forwardRef<SVGSVGElement, IconArchiveProps>((props, ref) => {
  return (
    <svg ref={ref} {...props}>
    </svg>
  );
});

4. svg currentColor 속성

CSS로 svg 색상을 제어하기 위해 svg currentColor 속성을 사용했습니다. svg에서 currentColor 속성을 활용하며 fill이나 stroke에서 css color 속성을 활용해 색상을 제어할 수 있습니다. fill="none", stroke="currentColor"로 색상을 부여하여 icon 내부 색상을 채우지 않고 icon 윤곽선 색상만 변경되도록 하였습니다.

import { HTMLAttributes, forwardRef } from 'react';

interface IconArchiveProps extends HTMLAttributes<SVGSVGElement> {}

const IconArchive = forwardRef<SVGSVGElement, IconArchiveProps>(
  (props, ref) => {
    return (
      <svg
        {...props}
        ref={ref}
        fill="none"
        viewBox="0 0 24 24"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
					...
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </svg>
    );
  }
);

export default IconArchive;

다음과 같은 방식으로 icon을 불러와 스타일을 부여할 수 있습니다.

import { IconArchive } from "@sopt-makers/icons";

// 1. in-line
<IconArchive style={{ width: "16px", height: "16px", color: "orange" }} />;

// 2. styled-component
export const Icon = styled(IconArchive)`
  width: 32px;
  height: 32px;
  color: red;

  :hover {
    color: blue;
  }
`;

🎨 스토리북

개발자, 디자이너의 원활한 작업과 소통을 위해 icon 라이브러리 스토리북을 배포하였습니다.

import * as Icons from "@sopt-makers/icons";

export default {
  title: "icons/Icons",
};

export const Default = {
  argTypes: {
    color: { control: "color" },
  },
  render: (props: { color: string }) => {
    const style = { width: 24, height: 24, color: props.color };

    return (
      <div className="icons-group">
        <h4>Communication</h4>
        <div>
          <Icons.IconArchive style={style} />
          <p>archive</p>
        </div>
        <div>
          <Icons.IconBookMark style={style} />
          <p>bookmark</p>
        </div>
        <div>
          <Icons.IconEdit style={style} />
          <p>edit</p>
        </div>
        ...
      </div>
    );
  },
};

결론

디자인 시스템을 구축하며 효율적인 개발과 협업을 위한 다양한 측면을 고려한 경험을 쌓을 수 있었습니다.

이전 프로젝트에서는 주로 기획안 기능 구현 작업을 진행했는데 디자인 시스템 개발이라는 새로운 영역에 도전할 수 있었습니다. 라이브러리를 만드는 과정을 통해 커뮤니케이션을 고려한 네이밍 규칙, icon의 확장성과 재사용성에 대해 고민해볼 수 있었으며 더 나아가 webpack의 작동 방식에 대해서도 공부해볼 수 있었습니다.

🙇‍♂️ 참고문헌

import-svgs-next-js-apps

[NEXTjs] svg를 ReactComponent처럼 사용하는 방법 (+storybook)