SVG "내보내기"에 대한 정리 - (2)

조경석·2022년 12월 18일
0
post-thumbnail

들어가기 전에 - Zeplin


지난 글에서 XD의 기본 옵션 내용만으로도 너무 길어져, 미처 정리하지 못한 XD에서 Zeplin으로 가이드를 작성한 경우의 내보내기 설정에 대해 조금 더 써보려 한다.
다른 툴 부분에서도 제플린 내보내기의 결과물을 같이 정리할 예정이다.

Zeplin
14년 배포를 시작한 디자인 가이드 툴, 왠만한 화면 디자인 툴보다도 역사가 긴 근본 툴로 디자인 가이드 툴의 표준을 지정한 수준이지만 가격 정책이 꽤 비싼 편이다.

UI 디자인 툴에서 특정 그래픽 그룹을 에셋으로 설정한 후 해당 페이지를 Zeplin으로 내보내면 개발자가 PNG, Webp, SVG등의 웹 환경에 최적화된 형태로 바로 다운로드 가능하다.
자체 도움말에서 내부적으로 SVGO를 사용한다고 안내하고 있으므로, XD 기본 설정으로 내보낸 SVG를 SVGO를 적용해도 큰 차이가 없다.

XD - Zeplin

XD의 경우 자체적인 SVG 내보내기 조건에 상관없이 고정된 형태로 내보내진다.

<svg data-name="XD 기본 설정" xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36">
    <defs>
        <linearGradient id="6glf85a4hb" x1=".5" x2=".5" y2="1" gradientUnits="objectBoundingBox">
            <stop offset="0" stop-color="#35f39d"/>
            <stop offset="1" stop-color="#169df6"/>
        </linearGradient>
        <filter id="5iu63kibya" x="9.5" y="6.5" width="19" height="25" filterUnits="userSpaceOnUse">
            <feOffset dy="1"/>
            <feGaussianBlur stdDeviation=".5" result="blur"/>
            <feFlood flood-opacity=".251"/>
            <feComposite operator="in" in2="blur"/>
            <feComposite in="SourceGraphic"/>
        </filter>
    </defs>
    <path data-name="사각형 1" style="fill:none" d="M0 0h36v36H0z"/>
    <g data-name="그룹 1" transform="translate(-930 -528)">
        <g style="filter:url(#5iu63kibya)">
            <rect data-name="사각형 2" width="16" height="22" rx="2" transform="translate(941 535)" style="fill:#e8eded"/>
        </g>
        <path data-name="패스 1" d="M13 6a3 3 0 0 0-3 3v5a1 1 0 0 0 2 0V9a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1h-6a1 1 0 0 0 0 2h6a3 3 0 0 0 3-3V9a3 3 0 0 0-3-3z" transform="translate(930 528)" style="fill:url(#6glf85a4hb)"/>
        <rect data-name="사각형 3" width="5" height="2" rx="1" transform="translate(945 538)" style="fill:#35f39d"/>
        <rect data-name="사각형 4" width="2" height="2" rx="1" transform="translate(951 538)" style="fill:#35f39d"/>
    </g>
    <rect data-name="사각형 5" width="8" height="11" rx="2" transform="translate(7 18)" style="stroke:#169df6;fill:#fff;stroke-width:2px"/>
    <circle data-name="타원 2" cx="1" cy="1" r="1" transform="translate(10 25)" style="fill:#169df6"/>
    <g data-name="타원 1" transform="translate(18 25)" style="stroke:#169df6;fill:none">
        <circle cx="1" cy="1" r="1" style="stroke:none"/>
        <circle cx="1" cy="1" r=".5" style="fill:none"/>
    </g>
</svg>

큰 차이점으로는 호환을 위한 xlink와 불필요한 전체 그룹이 삭제되고, 모든 도형에 의미없이 남아있던 id가 모두 삭제되었으며, 들여쓰기에 두칸 공백을 사용한다.
여전히 data-name과 tramsform이 남아있어 완벽히 압축된 형식은 아니며, 아래와 같은 장단점을 가지고 있다.

인라인 스타일 특성을 모두 style 특성 내에 css로 표기하는 형식으로 변경되었다. 스타일 특성이 다양하게 적용되어 있을 경우 이 형식이 더 압축된 형태일 수 있다.

* 테스트 중 XD편에서 확인했던 '내부 CSS 형식'으로 추출된 경우를 발견했으나, 패턴은 파악할 수 없었다. 제플린 내부적으로 용량을 비교하여 더 작은 방식을 지정하는 것으로 보인다. 이에 대한 사용자 옵션이 없어, 이는 스크립트에서 다양한 에셋을 컨트롤해야 할 경우 불편함을 초래할 수 있다.

필터와 그라디언트 id가 난수로 지정되어 있으며, 심지어 제플린 가이드를 갱신할때마다 id가 변경된다. SVG 필터를 스크립트에서 만질 일은 많지 않지만, 불편함을 초래할 가능성은 여전하다.

Figma

현재 사실상 표준으로 쓰이는 UI 디자인 툴로서 웹 환경에서 서비스되는 어플리케이션으로 작업자 환경에 구애받지 않는 호환성을 자랑한다. 몇 안되는 단점이라면 웹 어플리케이션 특유의 불안정성과, 아직 한글을 지원하지 않는다는 것 정도 뿐이다.
아래 아이콘이 Figma에서 스타일가이드를 따라 작업한 뒤 기본 설정으로 내보내기한 SVG 파일이다.

일단 코드부터 보자. Figma는 기본적으로 SVG 내보내기 시 들여쓰기를 적용하지 않고, 불필요한 id나 data-name을 남기지 않는다.
알아보기 쉽도록 코드 블럭에는 들여쓰기를 적용했으며, 도형 이름을 주석으로 표기했다.

<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
    <!-- Rect 2 -->
    <g filter="url(#filter0_d_1_1324)">
        <rect x="11" y="7" width="16" height="22" rx="2" fill="#E8EDED" />
    </g>
    <!-- 압축되지 않은 형태의 Path 1 -->
    <path d="M25 8C25.55 8 26 8.45 26 9V27C26 27.55 25.55 28 25 28H19C18.45 28 18 28.45 18 29C18 29.55 18.45 30 19 30H25C26.66 30 28 28.66 28 27V9C28 7.34 26.66 6 25 6H13C11.34 6 10 7.34 10 9V14C10 14.55 10.45 15 11 15C11.55 15 12 14.55 12 14V9C12 8.45 12.45 8 13 8H25Z" fill="url(#paint0_linear_1_1324)" />
    <!-- fill이 hex이 아닌 색상명으로 지정된 Rect 5  -->
    <rect x="7" y="18" width="8" height="11" rx="2" fill="white" stroke="#169DF6" stroke-width="2" />
    <!-- Rect 3 -->
    <rect x="15" y="10" width="5" height="2" rx="1" fill="#35F39D" />
    <!-- Rect 4 -->
    <rect x="21" y="10" width="2" height="2" rx="1" fill="#35F39D" />
    <!-- inner stroke에 맞춰 크기가 보정된 Circle 1 -->
    <circle cx="19" cy="26" r="0.5" stroke="#169DF6" />
    <!-- Circle 2 -->
    <circle cx="11" cy="26" r="1" fill="#169DF6" />
    <defs>
        <filter id="filter0_d_1_1324" x="10" y="7" width="18" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
            <feFlood flood-opacity="0" result="BackgroundImageFix" />
            <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
            <feOffset dy="1" />
            <feGaussianBlur stdDeviation="0.5" />
            <feComposite in2="hardAlpha" operator="out" />
            <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
            <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_1324" />
            <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_1324" result="shape" />
        </filter>
        <linearGradient id="paint0_linear_1_1324" x1="19" y1="6" x2="19" y2="30" gradientUnits="userSpaceOnUse">
            <stop stop-color="#35F39D" />
            <stop offset="1" stop-color="#169DF6" />
        </linearGradient>
    </defs>
</svg>

svg에 fill이 추가되어 있다. 간혹 UI 라이브러리에서 인라인 SVG에 대한 전역 스타일이 지정되어 있는 경우가 있는데, 이를 방지하기 위한 인라인 특성으로 보인다.

필터와 그라디언트의 id가 figma 내부 id를 사용하므로, 디자이너가 일부 사양을 변경한 후 재추출하여도 문제가 되지 않는다.

XD와 동일하게 center가 아닌 stroke가 적용되어 있는 경우, 도형의 크기를 보정하여 새로운 도형을 생성한다.
하지만 XD와 다르게 불필요한 빈 도형을 남기지는 않아 더 최적화되어 있다.

Figma에서 도형을 선택하고 enter를 클릭패 path 편집 모드로 진입할 경우 도형으로 변경된다.
도형의 Path를 편집하는 기능은 모든 UI 디자인 툴에 공통적으로 있는 기능이나, Figma의 경우 한번 path로 변경된 도형을 다시 도형으로 되돌릴 수 없도록 되어있다.
문제는 Rect에 한해 변마다 stroke를 변경할 수 있는 기능의 적용 여부를 제외하고 피그마 UI에서는 확인할 수 없어 내보내기를 한 이후에나 확인할 수 있다.
개발자가 SVG 코드를 확인한 후 디자이너에게 재작업을 요청하는 것은 너무 비효율적이므로, 디자이너가 꼭 확인해야 하는 부분이라 불편함이 있다.

Figma - Zeplin

피그마는 자체적인 가이드 시스템으로 개발자가 디자이너와 동일한 환경에서 스타일가이드 확인하고 에셋을 추출할 수 있다는 장점이 있다.
그렇기 때문에 Zeplin과 같은 서드 파티 가이드 툴을 사용할 이유가 없지만, XD와의 비교를 위해 Zeplin에서 추출한 결과를 비교해 보겠다.

<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
    <!-- Rect 2 -->
    <g filter="url(#ytwfnwzsza)">
        <rect x="11" y="7" width="16" height="22" rx="2" fill="#E8EDED"/>
    </g>
    <!-- 비교적 압축된 path 1 -->
    <path d="M25 8c.55 0 1 .45 1 1v18c0 .55-.45 1-1 1h-6c-.55 0-1 .45-1 1s.45 1 1 1h6c1.66 0 3-1.34 3-3V9c0-1.66-1.34-3-3-3H13c-1.66 0-3 1.34-3 3v5c0 .55.45 1 1 1s1-.45 1-1V9c0-.55.45-1 1-1h12z" fill="url(#i1r4c1y3jb)"/>
    <!-- fill이 더 짧은 hex로 다시 변경된 Rect 5  -->
    <rect x="7" y="18" width="8" height="11" rx="2" fill="#fff" stroke="#169DF6" stroke-width="2"/>
    <!-- Rect 3 -->
    <rect x="15" y="10" width="5" height="2" rx="1" fill="#35F39D"/>
    <!-- Rect 4 -->
    <rect x="21" y="10" width="2" height="2" rx="1" fill="#35F39D"/>
    <!-- inner stroke에 맞춰 크기가 보정된 Circle 1 -->
    <circle cx="19" cy="26" r=".5" stroke="#169DF6"/>
    <!-- Circle 2 -->
    <circle cx="11" cy="26" r="1" fill="#169DF6"/>
    <defs>
        <filter id="ytwfnwzsza" x="10" y="7" width="18" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
            <feFlood flood-opacity="0" result="BackgroundImageFix"/>
            <!-- 불필요한 type="matrix 삭제" -->
            <feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
            <feOffset dy="1"/>
            <feGaussianBlur stdDeviation=".5"/>
            <feComposite in2="hardAlpha" operator="out"/>
            <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
            <!-- 불필요한 mode="normal" 삭제 -->
            <feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_1324"/>
            <feBlend in="SourceGraphic" in2="effect1_dropShadow_1_1324" result="shape"/>
        </filter>
        <linearGradient id="i1r4c1y3jb" x1="19" y1="6" x2="19" y2="30" gradientUnits="userSpaceOnUse">
            <stop stop-color="#35F39D"/>
            <stop offset="1" stop-color="#169DF6"/>
        </linearGradient>
    </defs>
</svg>

XD때와 동일하게 필터와 그라디언트의 id가 난수로 변경된다.

불필요한 공백이나 1 미만의 특성값을 ".#"으로 표기하는 등 포맷 수준에서 압축이 적용되었다.

그림자 필터에 feColorMatrix, feBlend를 사용하는 피그마의 특성상, 불필요한 기본값인 type="matrix"이나 mode="normal" 선언이 삭제되었다.

환경에 상관없이 최선의 결과를 렌더링하는 벡터 형식인 SVG의 장점을 활용하기 위해, SVG에서 적용하는 그라디언트, 그림자 필터를 간단하게 알아보자.

그라디언트

과거 하이컬러 시대의 그라디언트 렌더링 자체가 최신 기술이던 시절의 역효과로 그라디언트가 촌스럽다는 평가가 대세가 된 적이 있었으나,
디스플레이의 색공간이 더욱 넓어지고 픽셀 밀집도가 커지면서 오히려 잘 만든 그라디언트가 디자인에는 필수적인 요소가 되었다.

XD
<linearGradient id="linear-gradient" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
  <stop offset="0" stop-color="#35f39d"/>
  <stop offset="1" stop-color="#169df6"/>
</linearGradient>

Figma
<linearGradient id="paint0_linear_1_1324" x1="19" y1="6" x2="19" y2="30" gradientUnits="userSpaceOnUse">
  <stop stop-color="#35F39D" />
  <stop offset="1" stop-color="#169DF6" />
</linearGradient>

XD는 gradientUnits="objectBoundingBox", Figma는 gradientUnits="userSpaceOnUse"를 사용한다.
좌표계에 익숙하다면 linearGradient에 설정된 좌표 특성만 봐도 감이 올텐데,
objectBoundingBox는 말 그대로 해당 필터가 적용된 도형의 영역(Bounding Box)를 기준으로, 좌상단 (0, 0)부터 우하단 (0, 0)까지의 상대 좌표를 기준으로 gradient의 기준선linear이 결정된다.
userSpaceOnUse는 최상단 좌표계, 즉 SVG의 width, height나 viewbox를 기준으로 절대 좌표계를 사용한다.(만약 둘 다 없다면 SVG 상위의 HTML 요소의 크기를 사용하는데, 당연히 오류를 발생시키기 쉽다.)
objectBoundingBox는 좌표 기준값이 단순한 경우 더 최적화된 형태지만, 여러 도형에 연속된 그라디언트를 사용하는 경우 절대좌표를 사용하는 userSpaceOnUse가 더 최적화된 형태일 수 있다.

그림자(Drop Shadow)

위에서 설명한 그라디언트에 대한 평가의 변화와 유사하게도, 과거 대부분의 앱에서 사용되었던 스큐어모피즘 역시 마테리얼 디자인, 뉴모피즘 등으로 대표되는 재해석이 이루어졌다.
비교적 계층이 적고 한 화면에서 최대한 다양한 기능을 보여주기 위해 형태의 익숙함을 디자인 기법으로 사용한 스큐어모피즘과는 달리,
실사로 투영된 듯한 형태를 통해 계층 구조에 대한 직관성을 갖추기 위해 실사로 투영된 듯한 시각효과를 UI에도 적극적으로 사용하는 시대가 되었다.

XD
<filter id="사각형_2" x="9.5" y="6.5" width="19" height="25" filterUnits="userSpaceOnUse">
  <feOffset dy="1" input="SourceAlpha"/>
  <feGaussianBlur stdDeviation="0.5" result="blur"/>
  <feFlood flood-opacity="0.251"/>
  <feComposite operator="in" in2="blur"/>
  <feComposite in="SourceGraphic"/>
</filter>

Figma
<filter id="filter0_d_1_1324" x="10" y="7" width="18" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
  <feFlood flood-opacity="0" result="BackgroundImageFix" />
  <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
  <feOffset dy="1" />
  <feGaussianBlur stdDeviation="0.5" />
  <feComposite in2="hardAlpha" operator="out" />
  <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
  <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_1324" />
  <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_1324" result="shape" />
</filter>

재미있게도, XD와 Figma의 그림자 적용 방식이 완전히 상이하다. Figma 위주로 XD와 비교하면서 정리해보자.

filterUnits

filter 태그에 적용된 filterUnits은 위에서 설명한 gradientUnits과 동일하다.
둘 모두 그림자 기본 영역을 설정하기 위해 상대 좌표를 사용하고, 적용한 drop shadow 조건인 blur 1의 상하좌우 크기를 적용해 위해 원래 도형과 위치, 크기가 조정되어 있다.
XD는 독특하게도 위치에 -0.5의 오차를 적용하고 도형 크기를 1씩 더 늘려 놓았다.

result, in

SVG의 filter의 하위 요소는 위에서부터 순차적으로 적용되고, 필터를 다양하게 조합하기 위해 result와 in을 사용한다.
result는 말그대로 해당 필터 요소(primitive)의 이름을 지정하는 역할이고,
in은 해당 필터 요소가 지정될 대상 요소 이름을 지정하는 특성으로, 기존 result로 선언된 이름을 사용하거나 SourceGraphic 값을 사용해 원래 이미지를 참조한다.
(in이 생략되어 있다면 자동으로 상위 요소를 참조한다.)
아래 효과별 설명에서 왜 이름이 이렇게 지어졌는지 직관적으로 이해할 수 있도록 강조 표시해 두었다.

feFlood

feFlood는 in으로 적용된 대상이나 지정된 좌표 영역에 특정 색상(flood-color)이나 특정 투명도를 덮어씌운다.
Figma에서는 먼저 feFlood를 적용해 투명도를 0으로 만든 뒤, 그 결과물(result)을 BackgroundImageFix로 선언해두었다.

feColorMatrix

이후 feColorMatrix를 다시 SourceGraphic에(in) 적용하고, 그 결과물(result)을 hardAlpha로 선언하는데,
이는 대상 영역의 색상을 조정하는 필터 요소로, 그림자를 그릴 필요가 없는 영역(hardAlpha)을 명시하기 위한 목적으로 사용되었다.

feColorMatrix의 색상 조정 계산법
type="matrix"은 values의 형식이 행렬임을 표기하는 특성으로, 기본값이므로 생략되어도 무방하고,
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"는 20개의 숫자의 배열로 채널이 4개이니
5개의 값을 각 픽셀별 색상의 채널(RGBA)별로 행렬 곱셈한 값의 합을 계산해 다시 적용한다.
이를 채널별 수식으로 표현하면 아래와 같다.
R' = R * (0 + 0 + 0 + 0 + 0)
G' = G * (0 + 0 + 0 + 0 + 0)
B' = B * (0 + 0 + 0 + 0 + 0)
A' = A * (0 + 0 + 0 + 127 + 0)

  • 왜 255도 아닌 127을 곱하는지는 알아내지 못했다. 애초에 SVG에서 A 채널 값은 0와 1 사이의 실수를 사용하므로 큰 숫자를 사용할 이유가 없다. 다른 호환성 이슈가 있는지는 찾는대로 추가할 예정이다.

feOffset

그 이후 대상을 이동시키는 feOffset으로, 가이드에 표기한 대로 drop shadow 오프셋인 y축으로 -1 이동을 적용한다.

feGaussianBlur

픽셀 단위 정규분포를 사용하는 가장 보편적인 흐림 효과인 Gaussian Blur을 통해 흐림 효과를 적용한다.
stdDeviation은 가우시안 흐림 효과의 편차를 지정하는 특성으로, 일반적인 drop shadow에서 흐림 편차를 지정하는 옵션은 사용하지 않으므로 0.5을 일반적으로 사용하는 것으로 보인다.
* 역으로 말하면, SVG 에셋에서는 흐림 편차를 디자이너 의도대로 조정 가능하다는 뜻이다.

feComposite

feComposite는 operator 특성에서 지정한 옵션에 따라 in과 in2를 합성한다.

Figma에서 사용한 operator="out"은 위에서 그린 feOffset과 feGaussianBlur를 적용한 결과(in이 생략되어 있다.)에서 in2로 지정한 hardAlpha영역을 제외하는 옵션이다.
말 그대로 그림자를 가지는 원래 도형으로 가려지는 불필요한 영역을 제외하는 용도로 사용되었다.

XD에서 사용한 operator="in"은 in과 in2 양쪽 모두에서 그려져 겹쳐진 영역만 남기는 옵션으로, 필터 영역에 그림자를 올리기 위해 사용했다.
그리고 한번 더 feComposite를 사용해 operator특성을 생략했을 시 적용되는 기본값인 operator="over"를 적용하여
SourceGraphic를 그림자 위에 덮는 형식으로 그려두었다.

Figma는 이후에 feColorMatrix를 한번 더 적용해 그림자 색상을 적용하며,
XD는 마지막 feComposite 이전에 feFlood에 flood-opacity와 flood-color를 적용해 통해 그림자 색상을 적용한다.

feBlend

마지막으로 Figma에서는 자체적으로 Drop Shadow와 도형 모두에 Blend Mode를 설정할 수 있기 때문에 feBlend를 통해 그림자를 합성한다.
제일 먼저 만들어두었던 BackgroundImageFix를 blend를 적용해 effect1_dropShadow_1_1324로 선언해둔 뒤,
최종적으로 원래 도형의 blend를 적용해 합성하는것으로 마무리하는 것을 볼 수 있다.

Figma는 불필요할 수 있는 feBlend를 사용해 합성하지만, 원래 도형 영역을 제외하기 때문에 불필요한 부하를 일부 줄일 수 있고,
XD는 편집 기능이 제한적이기 때문에 오히려 간결한 형식의 drop shadow를 사용한다고 볼 수 있겠다.

양쪽 모두 장단점이 있기 때문에 어느 쪽이 좋다고는 말할 수 없으며, SVGO는 필터 영역에 대해서는 문법 외에는 최적화하지 않으므로 최선의 최적화는 결국 필요에 따라 SVG를 직접 편집하는 것 뿐이다.

마무리

디자인 툴에서는 그냥 옵션 하나만으로 적용하는 그림자 효과가 전혀 다른 방식으로 적용되는건 예상하지 못한 부분이었다.
그저 피상적으로만 이해해 디자인 의도만 남긴 최적화된 형태를 만들지 못하는 부분이 불만이었는데,
이번에 두 디자인 툴에서 구현한 형식을 비교 분석해보며 SVG의 필터 적용 구조에 대해 이해하는 기회가 되었다.
다음 글에서는 이번 글에서 미처 정리하지 못한 Sketch의 경우와, 이 시리즈를 작성하면서 알게 된 SVG의 추가적인 기능들을 정리해보려고 한다.

0개의 댓글