SVG 구조, 렌더링 성능 알고 쓰기

ggong·2021년 11월 10일
9
post-thumbnail

웹페이지를 구성하면서 이미지나 아이콘을 사용할때 svg를 많이 쓴다. 디바이스 화면에 따라 2x, 3x처럼 최적화된 화질의 이미지를 따로 설정해주는 것은 아주 귀찮고, 벡터 기반으로 되어 있는 svg는 이런 부분을 따로 고려할 필요가 없기 때문이다. 보통은 svg 이미지 자체를 가져다 <img> 태그 소스나 백그라운드 속성으로 사용했지만 이번에 작업하면서는 svg들을 개별 컴포넌트화해서 사용하기로 했다. 그 과정에서 자세하게 찾아본 내용을 소개해보려고 한다!

svg는 뭐가 다른데?

svg(Scalable Vector Graphics)는 2차원의 벡터 그래픽을 표현하기 위한 XML 기반의 파일 형식이다.
비슷한 이미지 파일 형식인 png는 비트맵 기반의 이미지로, 각 픽셀에 저장된 비트 정보가 집합된 형태로 구성된다. 픽셀들의 배열 방식과 픽셀의 숫자와 비율이 디스플레이의 해상도를 결정하는데, 픽셀의 수가 많아질수록 화질은 뛰어나지만 용량 또한 커지게 된다. 또 해상도 이상으로 이미지를 확대하거나 축소하면 일반적으로 말하는 깨짐 현상이 발생할 수밖에 없다.

반면 svg는 벡터 그래픽 포맷으로, 그래픽의 형태들이 고정된 비트맵을 가진 것이 아니라 수학적 함수를 이용하여 도형이나 선을 그린다. 두 개의 점에 대한 정보를 갖고 있으면 선을 표현할 수 있고, 세 개 이상의 선에 대한 정보를 갖고 있으면 면을 형성할 수 있다. 벡터 그래픽은 실제로 이미지에 표현되는 점에 대한 정보를 저장하여 수학적 공식에 따라 점과 점을 연결하고, 선의 두께, 색상, 곡률 등을 값으로 지정해 이미지를 표현하는 방식이다.

SVG는 XML 기반 언어이기 때문에 사람이 코드를 읽기 쉽고, 개발자가 바로 수정도 가능하다.

svg 레이아웃

SVG 코드가 화면에 렌더링 되고, 레이아웃 되는 영역은 아래 값에 영향을 받는다.

- SVG 캔버스(Canvas) 혹은 뷰포트(Viewport)
- SVG 뷰박스(viewBox)

<svg 
  width="512" height="512" 
  viewBox="0 0 512 512">
  ...
</svg>

viewPort는 HTML 문서에서 svg가 렌더링되는 공간을 말한다. svg 렌더링은 설정된 캔버스(=viewPort)의 너비, 높이 안에서만 이루어진다.

viewBox는 뷰포트에 맵핑되는 공간을 명시한다. 값으로는 min-x, min-y, width, height를 지정할 수 있다. 뷰포트와 뷰박스 사이즈가 다를 경우, 그래픽의 가로:세로 비율을 유지하거나 크기를 확대/축소하는 스케일링 설정을 할 수 있는데 이때는 preserveAspectRatio 속성을 사용한다.

<svg
  width="512" height="512"
  viewBox="0 0 512 512"
  preserveAspectRatio="xMaxYMin meet">
  ...
</svg>

preserveAspectRatio 속성 값으로는 두 가지의 매개변수를 받을 수 있는데, <align>(정렬)<meetOrSlide>(채우거나 자름)이다. 각 매개변수 값들을 어떻게 지정하느냐에 따라 뷰포트 안에 뷰박스가 어떤 위치에 자리하느냐를 지정할 수 있다.

위 예시에서 지정된 preserveAspectRatio="xMaxYMin meet"는 아래 의미와 같다.

  • 가로:세로 비율을 유지함. 뷰포트의 오른쪽 x, 위쪽 y 값에 위치시킴
  • 뷰박스와 뷰포트 크기가 다를 경우, 뷰박스를 뷰포트 안에 모두 표시함

preserveAspectRatio의 더 많은 속성은 여기서 확인 가능하다.

svg로 도형을 그리기 위해서 자주 사용되는 기본 태그들은 아래와 같다.

- 사각형(rect)
- 원(circle)
- 직선(line)
- 다각형(polygon)

그리고 각 도형의 스타일을 제어하기 위한 속성은 아래와 같다.

- 너비(width)
- 높이(height)
- 색상(fill)
- 뷰포트 내부 특정 지점으로 위치(x, y)
- 둥근 테두리(rx, ry)
- 테두리 색상(stroke)
- 테두리 두께(stroke-width)

만약 특정 svg 소스를 개발자가 수정해서 내부 컬러를 변경하고 싶다면, fill 속성이나 stroke 속성을 변경하면 된다.

<svg>
  <rect width="480" height="240" fill="#3d87fb" x="20" y="40" rx="20" ry="20" />
</svg>

<svg>
  <circle 
    cx="200" cy="200" r="50" 
    fill="none" stroke="#f9b10a" stroke-width="14" />
</svg>

또한 svg에서는 더 복잡한 도형을 그리기 위해 <path> 도형을 사용할 수 있다. <path> 도형은 패스 데이터(d)를 통해 도형의 모양을 그리는데, 이 패스 데이터에는 패스를 그리는 moveTo, lineTo, closePath, curve 등을 의미하는 각 명령 이름과 좌표값이 들어있다. 데이터에 명시된 좌표값과 명령문을 이용해 도형을 이루는 선을 그리게 된다.

<svg>
  <path 
    d="M248.761,92c0,9.801-7.93,17.731-17.71,17.731c-0.319,0-0.617,0-0.935-0.021c-10.035,37.291-51.174,65.206-100.414,65.206 c-49.261,0-90.443-27.979-100.435-65.334c-0.765,0.106-1.531,0.149-2.317,0.149c-9.78,0-17.71-7.93-17.71-17.731 c0-9.78,7.93-17.71,17.71-17.71c0.787,0,1.552,0.042,2.317,0.149C39.238,37.084,80.419,9.083,129.702,9.083c49.24,0,90.379,27.937,100.414,65.228h0.021c0.298-0.021,0.617-0.021,0.914-0.021C240.831,74.29,248.761,82.22,248.761,92z" 
    fill="#f9ef21" stroke="#f9cf01" stroke-width="7" stroke-linejoin="round" />
</svg>

svg의 컨테이너 요소

  • <g> 요소 : 그룹화를 위한 컨테이너 요소.
  • <use> 요소 : 문서 전반에서 요소를 재사용할 수 있다. xlint:href="#식별자ID"처럼 쓰면 재사용할 요소를 호출하여 사용할 수 있다.
  • <defs> 요소 : <defs> 요소내에 선언된 그래픽은 svg 뷰포트에 렌더링되지 않는다. 렌더링하려면 <use>를 통해 참조해야 한다. 약간 js의 변수 선언 같다는 느낌을 받았다.

아래 코드로 컨테이너 요소들을 사용하는 방법에 대한 예시를 알아볼 수 있다.

<svg width="512" height="512" viewBox="0 0 512 512">
  
  <!-- 그래픽 정의 defs -->
  <defs>
    <!-- 그래픽 3개를 묶은 그룹 요소 선언 -->
    <g id="cherry-tree-group">
      <g id="fruit-cherry" transform="translate(0 100) scale(0.3)">...</g>
      <use xlink:href="#fruit-cherry" x="50" y="100" />
      <use xlink:href="#fruit-cherry" x="150" />
    </g>
  </defs>
  
  <!-- defs 내부에 선언된 그래픽 3개 그룹 재사용 -->
  <use xlink:href="#cherry-tree-group" />
  <use xlink:href="#cherry-tree-group" x="200" y="100" />
  
</svg>

svg 사용 케이스별 성능 비교

나는 애플리케이션에 이미지나 아이콘을 사용할 때, svg 파일 자체를 불러와서<img> 태그에 넣거나 css background-image 속성으로 자주 구현했었다. 사실 그동안은 성능이나 버그에 대해 고민해본 적이 없었는데, 이번에 신규 프로젝트를 하면서 약간의 문제가 생겼다. 아이콘을 디폴트 svg 하나(A), hover시 디자인 변경되는 svg 하나(B) 총 2벌 사용중이었는데, hover 하는 시점에 B 이미지를 가져오다보니 네트워크 상태에 따라 이미지 로딩이 지연되면서 순간적인 깜빡거림이 발생했다.

위 이슈와 관련해서 svg의 성능에 대한 이런저런 자료를 찾아보다가 아래 링크를 발견했는데,
위의 버그와 직접적인 관계는 없지만 나름 재밌게 읽었어서 잠깐 요약해 보려고 한다.

Which SVG technique performs best for way too many icons?

많은 양의 icon을 사용하는 경우, svg 사용법에 따른 성능 비교를 위해 1000개의 아이콘을 테스트한 내용이다.
여기서는 두 세트의 아이콘을 사용했는데, 하나는 불필요한 그룹 요소나 마스크 및 속성을 제거한 최적화된 아이콘 svg이고 또 하나는 최적화되지 않은 버전이라고 한다.
테스트 페이지는 프레임워크에 의한 사이드 이펙을 피하기 위해 vanilla HTML, CSS, JS로 구현했다. 렌더링 시간은 HTML이 추가되기 전에 애니메이션 프레임에서 performance.now()를 호출하고, 렌더링 후에 애니메이션 프레임에서 다시 호출하는 방법으로 측정했다.

HTML에서 svg를 렌더링하는 여러가지 방법을 사용해서 비교한 결과는 다음과 같다.

Inline SVG

<svg viewBox="0 0 24 24" width="24" height="24">
  <!-- paths, shapes, etc. -->
</svg>

HTML svg 엘리먼트 그대로 사용했을 경우 (번들링이나 스프라이트 처리 X).
최적화된 아이콘의 경우, inline SVG가 성능적으로 가장 좋았다. 반면 최적화되지 않은 경우에는 가장 느리다.
여기서 테스트했던 '최적화된 svg'라는 부분이 어떤 처리를 한건지 궁금했다. (블로그에서는 어떤 기술을 사용하든 SVG를 최적화하면 성능이 향상된다는 내용과 함께 svgo 와 같은 자동화 도구를 소개하고 있다)

Symbol Sprite

<svg style="display:none">
  <symbol id="example" viewBox="0 0 24 24" width="24" height="24">
    <!-- paths, shapes, etc. -->
  </symbol>
</svg>

<svg><use href="#example"/></svg>

svg를 표현하는 symbol 엘리먼트를 만들고, 개별 아이콘은 해당 symbol을 참조하여 표시된다. 이 방법의 성능은 렌더링하는 엘리먼트의 개수에 따라 영향을 받는다.

img 태그 사용

<img src="path/to/icon/color.svg" alt="">

일반적으로 많이 사용하는 방법인데, inline SVG와 함께 img 태그도 성능면에서 가장 좋은 결과를 보였다. 신기했던 게 img 태그를 사용하여 svg를 사용하면 인라인 svg와 약간 차이가 발생했다. 아래 이미지를 보면, 위가 inline SVG로 표현된 이미지이고 아래가 img 태그를 사용한 케이스다. 이미지 간격이 미세하게 다른 것을 알 수 있다. (왜지?)

data URI를 사용한 img 태그 사용

<img src="data:image/svg+xml,..." alt="">

svg 파일은 마크업 텍스트로 되어 있기 때문에, base64 인코딩 없이 data URI 스트링 형태로 쉽게 변환이 가능하다. 이렇게 쓰는 케이스는 생각해본적 없었는데, 의외로 성능면에서는 좋은 결과를 보였다.

Background image

<div style="background-image: url(path/to/icon/color.svg);">...</div>

백그라운드 이미지는 img 태그나 inline SVG보다는 성능이 떨어졌다. (경로로 사용하거나 data URI 사용시 모두)

Mask image

<div style="
  -webkit-mask-image: url(path/to/icon.svg);
  mask-image: url(path/to/icon.svg);">
  ...
</div>

CSS mask 속성은 아이템이 부분적으로만 보여지게 하거나 혹은 완전히 가려서 보여지지 않게 할 수 있는 기능이다. mask-image 속성을 사용하면 요소의 마스크 레이어로 사용되는 이미지를 설정할 수 있다. 나는 사용해본적 없는 기능인데, 이렇게도 이미지 표현을 할 수 있구나 하나 배웠다. 성능은 그닥.

여기까지 보고 느꼈던 결론은 simple is the best라는 것... 그리고 내가 자주 썼던 background-image는 성능 면에서 아주 가벼운 방법은 아니라는 것!



참고 :
Graphics ARIA Guidebook (https://a11y.gitbook.io/graphics-aria/svg-graphics/svg-container-elements)
벡터 그래픽(Vector Graphic)과 SVG(Scalable Vector Graphics) (https://untitledtblog.tistory.com/52)

profile
파닥파닥 FE 개발자의 기록용 블로그

1개의 댓글

comment-user-thumbnail
2021년 11월 15일

svg 를 사용하기 앞서서 귀감이 되는 글 이었읍니다. 따듯한 겨울 보내십쇼.

답글 달기