(번역) React.js에서 아이콘을 관리하는 "가장 좋은" 방법

sumi-0011·2023년 10월 4일
1

원문 : The "best" way to manage icons in React.js

Icon Rendering Techniques

오랫동안 사용자 인터페이스를 구축해 왔다면 아이콘은 어디에나 있다는 것을 아실 것입니다.  저는 지난 7년 동안 React.js로 UI를 구축해 왔으며 아이콘 관리를 위해 다양한 기술을 시도해 왔습니다. 각 기술에는 서로 다른 장단점이 있습니다. 제가 사용한 주요 기술은 다음과 같습니다.

  1. 이미지 + SVG
<img src='icon.svg'>
  1. 인라인 SVG
const icon = (
  <svg viewBox="0 0 24 24" width={16} height={16}>
    <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
  </svg>
)
  1. SVG Sprite를 사용한 인라인 SVG
const icon = (
  <svg viewBox="0 0 24 24" width={16} height={16}>
    <use href="sprite.svg#icon" />
  </svg>
)

위 중 SVG Sprite를 사용한 인라인 SVG는 최고의 성능을 제공합니다.

Tradeoffs

우리의 가장 큰 절충 요소는 성능과 사용자 경헙입니다.
"음, 성능은 사용자 경험의 일부입니다" 라고 말하기 전에, 나도 알고 있습니다(동의합니다). 계속 진행하겠습니다.

1️⃣ 이미지 + SVG

 이미지 자산(png, svg 등)을 참조하는 이미지 태그를 사용할 때, 페이지를 처음 다운로드하는 상황에서는 아이콘이 렌더링되기 전에 깜박입니다.

이는 request waterfall때문에 발생합니다.
먼저 브라우저는 HTML 문서를 다운로드합니다. 그런 다음 페이지의 모든 자산 (image, script, stylesheet 등)을 가져오도록 후속 요청을 수행합니다.

외부 자산을 참조하면 캐싱이 가능
브라우저(또는 CDN)는 자산을 캐시하고 후속 요청에서 참조할 수 있다.

이 기술은 후속 요청에 최적화 되어있습니다. 하지만 초기 로딩 환경에서는 바람직하지 않습니다. (처음 다운로드하는 상황에서 아이콘이 렌더링되기전에 깜빡인다. )

또한, 이미지 태그에서 참조될 때 CSS를 사용하여 SVG의 스타일을 지정할 수 없다는 단점 또한 존재합니다.

2️⃣ inline SVG

위에서 언급한 문제를 해결하기 위해 자주 사용되는 기술은 svg를 HTML 문서에 인라인하는 것입니다.

이 기술은 브라우저가 HTML 문서를 다운로드할 때 이미지를 불러오기위해 2차 요청을 할 필요가 없습니다. (깜빡임 없음)
그리고 CSS(win + win)를 사용하여 콘텐츠에 액세스하고 스타일을 지정할 수 있습니다.

하지만 이 기번에 함정이 없는것은 아닙니다.
SVG를 HTML 문서에 inline하면 문서가 상당히 커지고, 페이지에 요소가 추가되어 메모리 성능이 저하된다는 단점이 있습니다.
Google의 사람들이 이 주제에 관해 좋은 기사를 썼습니다 .

두번째 함정은 SVG가 JavaScript 번들 크기를 부풀린다는 것입니다.
브라우저는 렌더링하기 전 페이지의 JavaScript를 다운하고, parse하고, 평가해야합니다.
번들에 SVG를 포함하게 된다면 이 비용을 두번 지불하게됩니다. 다운로드하고 parse할 더 큰 번들이 있고 (일부 아이콘의 경우 복잡할수록 데이터가 크다), 렌더링된 HTML에 많은 요소를 추가합니다. (dom 탐색 속도 느려짐)

대부분의 React 애플리케이션은 이 두번째 방법을 사용합니다.
하지만 간단한 페이지의 번들 크기가 너무 커지는 문제가 있는 react-icons과 씨름한 후, 더 나은 방법이 있어야 한다는 것을 알게 되었습니다.

참고 : 사용하지 않은 아이콘을 번들에서 treeshake하는 방법이 있지만,
주의하지 않으면 react-icons는 5m짜리 JavaScript 번들을 제공하게 됩니다. 😱

3️⃣ SVG Sprite를 사용하여 icon rendering

세번째 기법인 SVG Sprites는, 위의 두가지 접근방식에서 언급한 대부분의 문제를 해결합니다.

image sprite를 기억할 만큼 나이가 많지 않을 수 있지만 (솔직하게 말하면 그렇지 않을 것입니다. ) 본질적으로 모든 이미지 자산을 단일 이미지에 넣고, (현재 아이콘이 표시된 곳에서) 좌표를 사용하여 특정 이미지를 참조하는 것입니다. 이는 많은 이미지 요청을 피하기 위해 사용된 성능 기술입니다. (HTTP 2가 대부분 해결한 문제) 이 기술은 비슷하지만, 다릅니다.

The Symbol Element

<symbol>요소를 설명하겠습니다.

symbol element는 "<use> 요소로 인스턴스화할 수 있는 그래픽 템플릿 개체를 정의하는데 사용합니다." - MDN.
<defs> 요소와 결합하면 아이콘으로 svg sprite를 구성할 수 있습니다.

첫번째로 sprite.svg 파일을 만들고, defs 요소와 <symbol>을 감싸는 <svg>요소를 추가합니다.
다음으로 아이콘을 가져와 svg를 symbol element로 바꾼 후 id를 부여합니다. (이 id는 매우 중요합니다!!)

마지막으로 sprite에 두번째 아이콘을 symbol로 추가합니다.

예시:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
    <symbol viewBox="0 0 24 24" id="icon-1">
      <path d="M6.84 5.76L8.4 7.68H5.28l-.72 2.88H2.64l.72-2.88H1.44L0 13.44h3.84l-.48 1.92h3.36L4.2 18.24h2.82l2.34-2.88h5.28l2.34 2.88h2.82l-2.52-2.88h3.36l-.48-1.92H24l-1.44-5.76h-1.92l.72 2.88h-1.92l-.72-2.88H15.6l1.56-1.92h-2.04l-1.68 1.92h-2.88L8.88 5.76zm.24 3.84H9v1.92H7.08zm7.925 0h1.92v1.92h-1.92Z" />
    </symbol>
    <symbol viewBox="0 0 24 24" id="icon-2">
      <path d="M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z" />
    </symbol>
  </defs>
</svg>

우리는 이 파일에서 모든 아이콘을 symbol로 계속 정의할 수 있습니다. (파일 크기를 고려해 125kb 이하로 유지하기).

다음으로 우리는 sprite를 참조할 React Component 요소를 생성할 것 입니다.

예시:

import React from "react";
import ReactDOM from "react-dom";

// keep a list of the icon ids we put in the symbol
const icons = ["icon-1", "icon-2"];

// then define an Icon component that references the 
function Icon({ id, ...props }) {
  return (
    <svg {...props}>
      <use href={`/sprite.svg#${id}`} />
    </svg>
  );
}

// In your App, you can now render the icons
function App() {
  return (
    <div className="App">
      {icons.map((id) => {
        return <Icon key={id} id={id} />;
      })}
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

사용 요소

위의 예시에서 놀라운 일은 "Fragment Identifier"(symbol에 정의한 id라고도 한다)에 연결된 <use>에서 발생합니다.

이제, 우리는 inline SVG로 할 수 있는 모든 작업(icon의 height, witdh, color 등)을 수행할수 있는 아이콘 구성요소입니다.
하지만, 모든 경로 데이터는 외부 자산 (JavaScript 번들이 아닌)에 저장됩니다.

보너스 팁

성능 향상을 위해 sprite.svg 파일을 미리 로드(또는 캐시)할 수 있습니다.
"특정 리소스를 미리 로드하면 브라우저가 현재 페이지에서 중요하다고 확신하기 때문에 검색하는 것보다 빨리 가져오겠다고 브라우저에 알립니다." - web.dev.

sprite를 미리 로드하려면, 문서의 머리 부분에 문서의 head에 link tag를 추가하면됩니다.

<head>
  <link rel="preload" as="image/svg+xml" href="sprite.svg">
</head>

서버 구성에 따라 브라우저가 적절하게 캐시할 수 있도록 sprite.svg에 적절한 캐시 헤더가 설정되어있는지 확인해야 할 수 있습니다.

That’s it! Hope this was helpful.

profile
안녕하세요 😚

0개의 댓글