[최적화] SVG Sprite를 적용하면서 겪은 시행착오

김하은·2024년 5월 5일
0

문제 상황

1. 특정 페이지에 들어갈 때마다 아이콘을 불러올 때 네트워크 요청을 너무 많이 한다는 사실을 발견했다.

2. 특히 기술 스택 인풋을 열때 마다 네트워크 요청을 너무 많이 하는 것 또한 문제였다.


너무 많은 네트워크 요청을 하게되면 발생할 수 있는 문제점

뤼튼에 검색한 결과 웹 페이지에서 아이콘을 불러올 때 너무 많은 네트워크 요청을 하게 되면 다음과 같은 문제점들이 발생할 수 있다고 한다.

  • 페이지 로딩 속도 저하: 각 아이콘 파일을 불러오기 위한 네트워크 요청이 많아질수록, 웹 페이지의 로딩 시간이 길어질 수 있습니다. 이는 사용자 경험에 부정적인 영향을 미칠 수 있으며, 특히 모바일 사용자나 느린 인터넷 연결을 사용하는 사용자에게 더 큰 문제가 될 수 있습니다.
  • 서버 부하 증가: 각 사용자가 페이지에 접속할 때마다 많은 수의 파일을 요청하게 되면, 서버에 부하가 증가하여 성능 저하를 초래할 수 있습니다. 이는 트래픽이 많은 사이트에서 특히 문제가 될 수 있습니다.
  • 브라우저 동시 연결 한계: 브라우저는 동시에 일정 수의 HTTP 요청만 처리할 수 있으며, 이 한계를 초과하는 요청은 대기 상태로 들어가게 됩니다. 너무 많은 아이콘 파일을 불러오려고 하면 이러한 한계에 도달해 다른 중요한 요소의 로딩이 지연될 수 있습니다.
  • 데이터 사용량 증가: 모바일 사용자의 경우 네트워크 요청이 많아질수록 데이터 사용량이 증가하게 됩니다. 이는 데이터 제한이 있는 사용자에게 추가 비용이나 속도 제한을 초래할 수 있습니다.
  • SEO 영향: 웹 페이지의 로딩 속도는 검색 엔진 최적화(SEO)에 중요한 요소 중 하나입니다. 페이지 로딩 속도가 느려지면 검색 엔진 순위에 부정적인 영향을 미칠 수 있습니다.

이 문제를 해결하기 위해 SVG Sprite를 사용하기로 했다.


CSS image sprite vs SVG Sprite

CSS image sprite

이미지 스프라이트(image sprite)란 여러 개의 이미지를 하나의 이미지로 합쳐서 관리하는 이미지를 의미합니다.
CSS 이미지 스프라이트

  • 이미지 스프라이트를 사용하면 이미지를 다운받기 위한 서버 요청을 줄일 수 있음
  • 일반적으로 PNG, JPG와 같은 비트맵 이미지 형식을 사용함
  • CSS를 통해 특정 이미지 부분을 표시하는 방식으로 사용해서 이미지 내 개별 요소를 직접 조작하는 것은 제한적임
    코드 예시

SVG sprite

SVG 파일 내에 여러 개의 그래픽 요소(예: 아이콘)를 포함시키고, 필요한 부분만을 태그를 통해 재사용하는 기술입니다.
SVG Sprite 기법을 사용해 나만의 특별한 Icon 컴포넌트 개발

  • 여러 개의 SVG 파일을 개별적으로 로딩하는 대신 하나의 스프라이트 파일로 관리하면 HTTP 요청의 수가 줄어들어 페이지 로딩 시간을 단축시키는 효과가 있음
  • 비트맵 이미지 형식을 이용하는 것과 비교했을 때 SVG는 확장 가능한 벡터 그래픽으로 크기를 조정해도 이미지 품질이 유지된다는 장점이 있음
  • DOM의 일부로 취급되므로, CSS와 JavaScript를 이용해 이미지의 개별 요소를 조작하고 스타일을 변경할 수 있음
  • 이미지를 DOM의 일부분으로 삽입할 수 있어 초기 로딩 속도를 더욱 개선할 수 있음
    • 이게 무슨 말이냐면 웹 페이지가 로드될 때 서버에서 별도의 이미지 파일을 가져오는 대신, 필요한 SVG 이미지들이 이미 HTML 문서 내에 삽입되어 있기 때문에 추가적인 HTTP 요청이 필요 없다는 뜻으로
    • 이는 네트워크 요청의 수를 줄이고, 결과적으로 페이지 로딩 시간을 단축시킬 수 있음
    • 그런데, DOM의 길이가 길어지면 웹 브라우저가 DOM을 파싱하고 렌더링하는 데에는 시간이 많이 소요되니까 오히려 초기 로딩 속도가 느려지는 거 아닐까?
    • 네트워크 요청 시간과 DOM을 파싱하고 렌더링 하는 시간 중 어떤 걸 감소시키는 게 이득일까?

SVG Sprite 적용하기

방법 1) 외부 SVG Sprite 파일 사용

SVG 스프라이트 기법으로 사이트 성능 향상시키기(리액트에서 스프라이트 SVG 사용하기)
이 블로그 글에서 SVG Sprite를 적용하는 방법에 대해 잘 설명이 돼 있어서 그대로 따라 했다.

적용 예시

1. Spritebot을 이용해서 만든 SVG Sprite 파일(reactQuillIcons.svg)을 assets 폴더에 저장한다.

2. 만들어진 SVG Sprite 파일이 올바른지 확인하고 싶다면, SVG symbol viewer에 파일을 드래그 앤 드롭하여 미리 볼 수 있다.

3. SVG Sprite 파일의 경로와 sybol 태그의 id를 조합해서 특정 아이콘을 분리한다.

// \src\constants\reactQuillIcons.ts

import reactQuillSvgSprite from "@/assets/svgSprite/reactQuillIcons.svg";

export const QUILL_ICONS = {
  header1: {
    src: `${reactQuillSvgSprite}#header1`,
    alt: "제목1 아이콘",
  },
  header2: {
    src: `${reactQuillSvgSprite}#header2`,
    alt: "제목2 아이콘",
  },
  //중략
  link: {
    src: `${reactQuillSvgSprite}#link`,
    alt: "링크 아이콘",
  },
  image: {
    src: `${reactQuillSvgSprite}#image`,
    alt: "이미지 아이콘",
  },
};

4. 분리한 아이콘의 경로와 <use> 태그를 이용해서 SVG Sprite의 요소를 참조한다.

// \src\components\commons\ReactQuill\CustomToolbar.tsx
import styled from "styled-components";
import DESIGN_TOKEN from "@/styles/tokens";
import { QUILL_ICONS } from "@/constants/reactQuillIcons";
import { Quill } from "react-quill";

const { color, mediaQueries } = DESIGN_TOKEN;

const icons = Quill.import("ui/icons");
icons["header"]["1"] = `<svg class="fm_editor_icon"><use href=${QUILL_ICONS.header1.src} /></svg>`;
icons["header"]["2"] = `<svg class="fm_editor_icon"><use href=${QUILL_ICONS.header2.src} /></svg>`;
icons["header"]["3"] = `<svg class="fm_editor_icon"><use href=${QUILL_ICONS.header3.src} /></svg>`;
icons["blockquote"] = `<svg class="fm_editor_icon"><use href=${QUILL_ICONS.header1.src} /></svg>`;
icons["bold"] = `<svg class="fm_editor_icon"><use href=${QUILL_ICONS.bold.src} /></svg>`;
icons["italic"] = `<svg class="fm_editor_icon"><use href=${QUILL_ICONS.italic.src} /></svg>`;
icons["underline"] = `<svg class="fm_editor_icon"><use href=${QUILL_ICONS.underline.src} /></svg>`;
icons["image"] = `<svg class="fm_editor_icon"><use href=${QUILL_ICONS.image.src} /></svg>`;
icons["link"] = `<svg class="fm_editor_icon"><use href=${QUILL_ICONS.link.src} /></svg>`;
//중략

결과

before vs after

  • 네트워크 요청 수가 9번에서 1번으로 감소했다.
  • 브라우저 로딩 시간이 감소했다.
  • 로드하는 파일 크기가 27.3kB에서 10.2kB로 62.6% 감소했다.

방법 2) 인라인 SVG Sprite

인라인 SVG는 HTML에 직접 <svg> 태그를 사용하는 것을 말합니다.
SVG Security

기술 스택 아이콘에도 위와 같은 방법으로 svg sprite 파일을 사용하려고 했으나 예상치 못한 문제가 생겨서 다른 방법(인라인 SVG Sprite)을 찾게 됐다.

외부 SVG Sprite 파일을 사용할 때 발생한 문제

  • 외부 SVG Sprite 파일을 사용한 결과 Angular, Next.js, Python, Kotlin, Swift, Jira, TensorFlow 아이콘이 보이지 않거나 일부만 렌더되었다.
  • 비정상적으로 렌더링 되는 아이콘들의 공통점은 모두 SVG Sprite 파일의 <symbol>요소 내부에<linearGradient>를 포함하고 있다는 것이었다.

문제 해결법 찾기

  • 파이어 폭스에서는 Next.js를 제외하고 정상적으로 렌더링 됐다.

=> 이 문제가 chrome 브러우저 때문일 수 있다고 추측했다.

  • 이 문제의 정확한 원인을 찾고 싶었으나 찾지 못해서 뤼튼에 검색해본 결과 다음과 같은 답변을 얻었다.

    SVG 내에 정의된 <linearGradient> 요소는 SVG 스프라이트와 같이 외부 파일에 정의되어 있고, <use> 태그를 통해 참조될 때, 일부 브라우저에서는 정상적으로 렌더링되지 않을 수 있습니다. 이는 주로 <use> 태그가 외부의 SVG 스프라이트에서 정의된 gradient, mask, clip-path 같은 요소들을 참조할 때 발생하는 보안 제한 때문입니다.

    브라우저의 보안 모델은 다른 도메인 또는 동일 도메인에서조차도 외부 SVG 파일의 특정 요소(예: <linearGradient>, <mask>, <clipPath>)에 대한 참조를 제한하기 때문에 이런 방식에 문제가 발생할 수 있습니다.

  • 뤼튼에서 나온 내용을 검증하기 위해 자료를 찾아 보았지만 문서로 된 정확한 근거는 찾지 못했다. 깃헙 이슈에서 관련 문제로 토론한 내용을 보니 chrome 문제인 것 같았다.
    svg <use> tag frequently breaks on redraw in Chrome

여러 자료를 참고한 결과 인라인 SVG sprite로 문제를 해결할 수 있다고 해서 적용해 보았다.

SVG Sprite 기법을 사용해 나만의 특별한 Icon 컴포넌트 개발
SvgIcon 컴퍼넌트: svg sprite html에 임베드 방식
위 두개의 블로그에 설명히 자세히 나와있었다.

적용 예시

1. SVG Sprite 코드를 index.html에 삽입하는 컴포넌트 만들기

  • 첫 번째 단계로, SVG Sprite 코드를 createPortal 함수를 이용해서 DOM에 접근해 body에 삽입하는 컴포넌트를 만든다.
// \src\components\commons\GlobalStackSvgSprite.tsx
import { createPortal } from "react-dom";

const stackSvgSpriteCode = (
  <svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink">
    <symbol id="aws" viewBox="0 0 256 153">
      <path
        fill="#252F3E"
        d="M72.392 55.438c0 3.137.34 5.68.933 
      // 중략
  </svg>
);

export function GlobalStackSvgSprite() {
  return createPortal(stackSvgSpriteCode, document.body);
}
  • Spritebot을 이용해서 만든 SVG Sprite 파일을 jsx 파일로 변환할 때 몇 가지 수정이 필요하다.
    • xmlns:xlink속성을 xmlnsXlink로 변경한다.
    • 속성 이름들을 케밥 케이스에서 카멜 케이스로 변경한다.
    • <linearGradient> 태그에 중복되는 id가 있다면 고유한 id로 변경한다.

2. GlobalStackSvgSprite 컴포넌트를 App 컴포넌트에서 사용하기

  • App 컴포넌트에서 GlobalStackSvgSprite 컴포넌트를 사용해서 한번만 호출하도록 한다.
// \src\App.tsx
// 중략
function App() {
  return (
    <>
      <QueryClientProvider client={queryClient}>
        <GlobalStyles />
        <ReactQueryDevtools initialIsOpen={false} />
        <PageRouter />
        <GlobalStackSvgSprite /> // 컴포넌트를 여기에 삽입
        <Toasts />
      </QueryClientProvider>
    </>
  );
}

export default App;

3. <use> 태그와 SVG Sprite의 symbol 태그의 id를 이용해서 아이콘을 불러오기

  • 마지막으로, <use> 태그를 사용하여 필요한 아이콘을 SVG Sprite로부터 불러온다. 이때 symbol 태그의 id 값을 사용한다.
// \src\components\commons\Stack.tsx

//중략
export default function Stack({ stack, className }: StackProps) {
  const icon: Image =
    stack && stack.img
      ? {
          src: stack.img,
          alt: stack.name,
        }
      : ICONS.questionMark;

  return (
    <Background className={className}>
      <Icon>
        <use href={icon.src} /> // icon.src 예시) "#javascript"
      </Icon>
    </Background>
  );
}
//중략

const Icon = styled.svg`
  width: 100%; /* 컨테이너 너비에 맞춤 */
  height: 100%; /* 컨테이너 높이에 맞춤 */
  object-fit: contain;
`;

결과

before vs after

  • 네트워크 요청 수가 33번에서 0번으로 감소했다.

회고

  • 최종적으로 SVG Sprite를 이용해서 네트워크 요청 수를 줄였다.
  • 하지만 모든 아이콘에 대해 SVG Sprite를 적용하지는 않았으며, 이에 대한 결정은 팀원들과의 추가 논의가 필요할 것 같다.
  • 아이콘 이미지를 적용하는 과정에서, 현재 방식은 <img> 태그를 <svg> 태그와 <use> 태그로 일일이 변경해야 하는 번거로움이 있음을 깨달았다. 이를 해결하기 위해, 아이콘을 쉽게 최적화하고 관리할 수 있는 별도의 컴포넌트를 제작하는 방안을 고려하게 되었다.
  • ReactQuill의 아이콘에 SVG Sprite를 적용하는 과정에서 반복문을 활용하여 코드를 더 효율적으로 작성해야 겠다고 생각했다.
  • 최적화 작업이 실제로 성능 향상에 기여하는지를 검증하기 위해 Lighthouse와 같은 도구를 사용하여 성능을 보다 정확하게 분석해볼 필요가 있음을 느꼈다.
profile
아이디어와 구현을 좋아합니다!

0개의 댓글