NextJS에서 SVG에 대한 접근을 고민해보며

coggiee
10 min readJul 1, 2024

--

연관 포스트: SVG Sprite Hydration Error

모각GO 프로젝트를 진행하면서 고민해볼 만한 사항이 생겼습니다.

저희는 게이미피케이션 요소가 서비스의 주요 특징이었기에, 픽셀 아이콘을 사용해야 했습니다. 픽셀 아이콘을 제공하는 서비스(또는 라이브러리)는 한정적이었고 그나마도 필요한 아이콘을 모두 제공하지는 않았습니다.
여러 레퍼런스를 찾고 pixelarticons, streamlinehq라는 서비스를 사용하기로 결정했습니다.

어떻게 서비스를 사용할 것인가

서비스를 어떻게 사용할 것인가? 라는 고민이 생겼습니다.
두 서비스 모두 2가지 방식을 제공합니다.

  1. 별도의 라이브러리를 설치해서 사용한다.
  2. 홈페이지에서 제공하는 JSX(svg, jpg …)코드를 사용한다.

사용성에 있어서는 1번이 가장 간편합니다. 그저 라이브러리를 설치하고 원하는 아이콘을 import 하기만 하면 됩니다.
그러나 저희 서비스는 초기 로드 속도가 현저히 느렸고, 1번의 방식을 사용할 경우 프로젝트가 무거워져 로드 속도가 더 느려질 수 있다고 판단하여 배제했습니다.

2번 방식에 대해서도 고민이 생겼습니다. 평소 사이드 프로젝트를 할 때 2번 방식을 애용하는데, 굉장히 간편했습니다.
그러나 이번 프로젝트는 팀 단위 이었기에, 혼자만이 아닌 전체적인 사용성을 고려해야 했습니다.

2번 방식에서 장점과 단점에 대해서 여러 얘기가 오갔습니다.
일단, JSX 코드를 복사해서 사용하면 되기에 매우 간편하며, SVG 컴포넌트 내에서 props를 통해 동적으로 속성을 전달할 수 있습니다. (색상같은 스타일 변경에 용이)
그러나 각 아이콘이 하나의 파일로 대응되기에 유지보수가 힘들어지며, 여러 아이콘을 사용한다면 파일 상단에 import가 무수히 많아집니다.

가장 중요한 성능에 있어서는, JS 번들에 SVG가 포함되어 런타임 성능과 메모리 사용을 모두 헤친다고 합니다.
그래서 2번 방식은 일단 보류했습니다.

실제로 지금 현재 블로그 레포지토리에서 사용하고 있는 아이콘들입니다. 컴포넌트 형식으로 사용했더니 무수히 많은 파일을 볼 수 있습니다.

다른 방식을 찾아보며

위에서 언급했듯이, 다른 방식을 찾아볼 필요가 생겼습니다.
일단 서비스의 상황을 고려했을 때, 여러 아이콘을 페이지와 컴포넌트에서 사용하는 경우가 많았고 각자가 사용하는 곳이 모두 달랐기에 유지보수 측면에서 하나의 파일로 미리 만들어 놓고 사용하는게 좋을 것 같다고 생각했습니다.
마침 팀원분이 SVG Sprite 방식에 대해 공유해주셨고, 이에 대해서 찾아보았습니다.

SVG Sprite

정의에 대해 찾아보니 아래와 같이 SVG Sprite에 대해서 설명하고 있었습니다.

  • SVG sprites are a native way to group all your icons together and reference them from HTML without needing a templating system.
    → SVG 스프라이트는 템플릿 시스템 없이도 모든 아이콘을 그룹화해서 HTML에서 참조할 수 있는 기본 방법
  • SVG Sprite는 <svg /> 에 배치되는 <symbol /> 로 감싸여진 SVG 콘텐츠 모음

간단하게 보자면, 여러 개의 SVG 이미지를 하나의 파일에 모아 사용하여 다수의 아이콘이나 그래픽을 효율적으로 로딩하고 관리하기 위한 방식이라고 이해했습니다.

프로젝트에 적용해보며 (기존 JSX와의 차이?)

기존에는 JSX로 SVG 아이콘을 사용할 때 저는 아래와 같이 사용했습니다.

function IconBack(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 512 512"
fill="currentColor"
height="1em"
width="1em"
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={32}
d="M112 352l-64-64 64-64"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={32}
d="M64 288h294c58.76 0 106-49.33 106-108v-20"
/>
</svg>
);
}

export default IconBack;

상위에 <svg>태그로 감싼 걸 볼 수 있습니다.

SVG Sprite 방식은 아래와 같이 사용할 수 있었습니다.

const spriteSVG = (
<svg xmlns="http://www.w3.org/2000/svg" className="hidden">
<symbol
id="arrow-left-turn"
viewBox="0 0 20 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.3801 5.19012H16.8601V3.67012H7.71009V0.620117H6.19009V2.14012H4.67009V3.67012H3.14009V5.19012H1.62009V6.71012H0.0900879V8.24012H1.62009V9.76012H3.14009V11.2901H4.67009V12.8101H6.19009V14.3301H7.71009V11.2901H13.8101V12.8101H15.3301V14.3301H13.8101V15.8601H12.2901V17.3801H15.3301V15.8601H18.3801V14.3301H19.9001V6.71012H18.3801V5.19012Z"
fill="#000001"
/>
</symbol>
<symbol id="arrow-right-turn" ...>
// ... path
</symbol>
// ... other symbol
</svg>
)

일단 상위 태그가 <svg> 임은 동일합니다.
그러나 하위에서 개별적인 SVG 아이콘들은 <symbol>이라는 태그를 사용하고 이때 id를 부여받습니다.
해당 id를 통하여 원하는 SVG 아이콘을 사용할 수 있습니다.
자세한 설명은 참고한 블로그에서 잘 설명해주고 있어서 생략하겠습니다.

실제로 프로젝트에서 아래와 같이 사용했습니다.

interface IconProps {
id: string;
size?: number | string;
}

function Icon(props: React.SVGProps<SVGSVGElement> & IconProps) {
const { id, size = "100%", ...rest } = props; // 전달받은 tailwindcss, 정확히는 className을 rest로 가져옵니다.
return (
<svg width={size} height={size} {...rest}> // className을 설정합니다.
<use href={`#${id}`} />
</svg>
);
}

export default Icon;
// 실제로 Icon을 사용하는 컴포넌트
return (
<div className="flex gap-2">
<div className="flex h-full items-end">
<Icon id="jandi" className="mb-6 h-6 w-6" /> // 사용하고자 하는 아이콘의 id를 props로 전달합니다.
</div>
<div className="flex flex-col gap-2">
<YProgress value={jandiRate} background="green" />
<span className="text-xs">
{jandiRate < 0 ? "0" : jandiRate}%
</span>
</div>
</div>
);

사용해보며 느낀 점에 대해

일단 SVG Sprite 방식의 장점과 단점에 대해 찾아보았습니다.

장점

  • 성능 개선: 여러 개의 이미지 파일 대신 단일 파일을 로딩하기 때문에, 네트워크 요청의 수가 줄어들고 페이지의 로딩 속도가 개선됩니다.
  • 유지 관리 용이성: 모든 SVG 이미지가 하나의 파일에 있기 때문에, 이미지를 추가하거나 수정할 때 유지 관리가 용이합니다.
  • 캐싱 효율성: SVG 스프라이트 파일은 브라우저에 의해 캐시될 수 있으므로, 사용자가 다른 페이지로 이동할 때 재로딩 없이 이미지를 사용할 수 있습니다.

단점

  • 초기 로딩 시간: 스프라이트 시트가 크면 처음 페이지를 로딩할 때 파일을 다운로드하는 데 시간이 걸릴 수 있습니다. 하지만, 이는 캐싱을 통해 상쇄될 수 있습니다.
  • 복잡성 증가: 스프라이트 시트를 생성하고 관리하는 과정이 복잡할 수 있으며, 특히 큰 프로젝트에서는 이를 자동화하기 위한 추가적인 도구나 설정이 필요할 수 있습니다.

검색하면 찾을 수 있는 장점과 단점입니다.

개인적으로 성능상의 이점이 있는지 확인을 해보고 싶었는데, 수치를 찍어두지 못해서 개선이 이루어 졌는지는 확인하지 못해서 아쉽습니다. (개수도 적어서 성능상의 이점이 있었을까에 대해서는 고민이 됩니다.)

저희 프로젝트는 tailwindcss를 채택했는데, 아래와 같이 tailwindcss 스타일 속성도 전달이 가능했습니다.
(물론 아이콘을 가리키는 <symbol>내부에 fill=currentColor 등과 같은 처리를 해줘야했습니다.)

interface IconProps {
id: string;
size?: number | string;
}

function Icon(props: React.SVGProps<SVGSVGElement> & IconProps) {
const { id, size = "100%", ...rest } = props; // 전달받은 tailwindcss, 정확히는 className을 rest로 가져옵니다.
return (
<svg width={size} height={size} {...rest}> // className을 설정합니다.
<use href={`#${id}`} />
</svg>
);
}

export default Icon;

<symbol>에 미리 정해둔 <id>로 아이콘을 사용하기에, 유지보수 측면에서는 확실히 이점을 느낄 수 있었습니다.

  1. 하나의 파일에서 관리를 하기에 파일의 수가 줄어들었습니다.
  2. 새로운 아이콘이 필요한 경우 기존 파일에 코드만 추가하면 됩니다.
  3. 다수의 import문을 제거할 수 있다.

후에 다시 생각해보니, 2번은 이점이 맞는지 정확히는 SVG Sprite 방식만의 이점이 맞는지는 모르겠습니다. JSX로 사용할 때도, 코드를 단순히 복사하여 JSX를 생성하면 되는데 “Sprite 방식만이 가지는 이점이 맞을까” 라는 고민이 듭니다.

더 나아가서

spritebot이라는 툴도 발견했는데, 원하는 svg 파일을 업로드하면 이를 압축해서 하나의 Sprite sheet를 생성해줍니다.

실제로 적용해보니 약 50%정도로 크기가 압축되는데, 환경에 따라 압축률은 변하는 것 같습니다. 그러나, 새로운 SVG 파일을 사용하게 된다면 기존의 사용하고 있는 모든 SVG 파일을 다시 업로드하여 Sprite Sheet를 생성해야 하는 복잡함이 수반됩니다.

참고

SVG-in-JS와의 결별
SVG Sprite 방식 사용하기
Simple icon systems using SVG Sprite

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

coggiee
coggiee

Written by coggiee

0 Followers

Just doing

No responses yet

Write a response