디자이너가 공유 드라이브에 .svg 아이콘 12개를 방금 떨어뜨려 놓았다. 각 파일은 6~12 KB. 텍스트 에디터로 하나 열어 보면 첫 70줄은 <sodipodi:namedview>, <inkscape:perspective>, XML 처리 명령어, 익스포트 플러그인이 남긴 주석, 자동 생성 ID가 붙은 3겹 <g>, 그리고 어떤 요소도 참조하지 않는 그래디언트가 들어 있는 <defs> 4개. 진짜 아이콘은 90바이트짜리 path 한 개다.
SVG는 이미지 포맷 중에서도 특별하다 — 파일 자체가 소스 코드이기도 하다. Illustrator, Figma, Sketch, Inkscape 어떤 익스포트 도구든 지문을 남긴다. 하지만 JPEG나 PNG와 달리 그 지문은 주소를 매길 수 있다 — 읽을 수도, 지울 수도, 결정론적으로 처리할 수도 있다. 그게 SVGO가 하는 일이다. ZeroTool의 SVG 최적화 도구는 SVGO를 그대로 브라우저에서 돌리되, 가장 결정하기 까다로운 플러그인 선택만 컨트롤 패널로 빼두었다.
이 가이드는 SVGO가 실제로 무엇을 제거하는지, 프로덕션에서 아이콘을 조용히 망가뜨리는 두 플러그인은 무엇인지, <style>과 <script> 블록은 어떻게 다뤄야 하는지, 그리고 브라우저에서 조정한 설정을 그대로 번들러나 CI에 어떻게 옮기는지를 차례로 짚는다.
바이트는 어디 숨어 있나
전형적인 24×24 Figma 익스포트 아이콘의 모습:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="24" height="24" viewBox="0 0 24 24" fill="none">
<!-- exported from Figma 2026.5 -->
<g id="icon">
<g id="path-group">
<path id="Vector"
d="M11.99999 2.00001 L21.99999 12.00001 L11.99999 22.00001 L2.00001 12.00001 L11.99999 2.00001 Z"
stroke="#000000" stroke-width="2.000" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
</svg>
이 8줄에는 적어도 6가지 최적화 포인트가 숨어 있다:
| 무엇 | 담당 플러그인 |
|---|---|
선언만 되고 사용되지 않은 xmlns:xlink | removeUnusedNS |
| 디자이너 주석 | removeComments |
자동 생성됐고 어디에서도 참조되지 않는 id="icon" / id="path-group" / id="Vector" | cleanupIds |
보존할 가치 있는 속성을 갖지 않는 두 겹의 <g> 래퍼 | collapseGroups |
2.000처럼 적힌 stroke-width를 2로 | cleanupNumericValues |
11.99999 같은 부동소수점 꼬리 | cleanupNumericValues + floatPrecision: 3 |
#000000을 #000으로(또는 테마 가능하게 하려면 currentColor로) | convertColors |
preset-default 컬렉션은 위 항목을 모두 기본 활성화한다. 같은 SVG를 SVG 최적화 도구에 넣고 기본 토글 그대로 한 번 돌리면 대략 이런 출력이 나온다:
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m12 2 10 10-10 10L2 12 12 2Z"/></svg>
700바이트 정도가 200바이트 정도로 — 손으로 쓴 SVG 또는 디자인 도구 익스포트에서 흔한 절감폭이다. 이미 프로덕션에서 minify된 아이콘은 5~15% 정도로 줄어드는 경우가 많다. 쉬운 부분은 상류에서 이미 잘려 나갔기 때문이다.
preset-default가 실제로 돌리는 것
기본 8개 주 토글을 그대로 두고 「모든 34개 플러그인 표시」 패널은 건드리지 않은 상태에서 SVG 최적화를 클릭하면, SVGO는 다음 순서로 preset-default 체인을 실행한다([email protected]로 검증):
removeDoctype removeXMLProcInst removeComments
removeDeprecatedAttrs removeMetadata removeEditorsNSData
cleanupAttrs mergeStyles inlineStyles
minifyStyles cleanupIds removeUselessDefs
cleanupNumericValues convertColors removeUnknownsAndDefaults
removeNonInheritableGroupAttrs removeUselessStrokeAndFill
cleanupEnableBackground removeHiddenElems removeEmptyText
convertShapeToPath convertEllipseToCircle moveElemsAttrsToGroup
moveGroupAttrsToElems collapseGroups convertPathData
convertTransform removeEmptyAttrs removeEmptyContainers
mergePaths removeUnusedNS sortAttrs
sortDefsChildren removeDesc
대부분은 구조적으로 안전하다 — 사양상 「시각적 부작용 없음」으로 정의된 것만 제거한다. 다만 다음 셋은 별도로 짚을 가치가 있다. 명확하지 않은 방식으로 렌더링을 바꿀 수 있고, 문제는 보통 무언가 깨진 뒤에야 보인다.
**inlineStyles**는 <style> 블록의 규칙을 요소 속성으로 옮긴다. 3개 클래스를 대상으로 하는 5줄 stylesheet가, fill · stroke · opacity를 인라인으로 가진 3개 요소로 변한다. HTML에 임베드되어 currentColor로 색을 제어하는 아이콘이라면 이게 정답이다. 외부 stylesheet가 런타임에 .icon-primary { fill: red; }로 색을 덮어쓸 것을 기대한다면 곤란하다 — 덮어쓸 대상 클래스가 더는 존재하지 않는다.
**convertPathData**는 모든 path의 d 속성을 더 짧은 명령어와 상대 좌표로 다시 쓴다. 정밀도 경계에서는 수학적으로 손실이 있다. 베지어 제어점이 0.001 단위만큼 어긋나면, 매우 큰 사이즈로 렌더링했을 때 시각적으로 다른 곡선이 될 수 있다. 아이콘 사이즈에선 기본 floatPrecision: 3로 충분하다. 2000px 폭으로 렌더링되는 히어로 일러스트라면 4 또는 5까지 올리는 편이 안전하다.
**cleanupIds**는 같은 파일 안에서 <use>, <style>, 다른 url(#…) 참조에 잡히지 않은 ID를 제거한다. 아이콘이 스프라이트 시트의 일부로 출하되어 다른 파일이 #icon-name을 참조한다면 — SVGO는 그 참조를 보지 못하고 ID를 그냥 지운다. 스프라이트 소스 파일을 다룰 때는 주 토글에서 cleanupIds를 끄거나, 「모든 34개 플러그인 표시」 패널에서 개별로 끄면 된다.
반응형을 망가뜨리는 두 토글
워크벤치의 주 토글 8개는 배치가 의도적이다. 앞쪽 6개는 거의 건드릴 일 없는 안전한 기본값, 따로 경고가 필요한 두 개는 일부러 둘째 줄에 있다 — viewBox 제거와 width/height 제거다.
둘 다 기본 비활성, 경고 표시가 붙어 있다. 둘 다 분명 최적화 항목이지만, 임베딩 컨텍스트에 따라 아이콘을 완전히 못 쓰게 만들 수도 있다.
viewBox="0 0 24 24"는 SVG가 「이 이미지의 내부 좌표계는 너비 24 단위 × 높이 24 단위」라고 선언하는 부분이다. 이걸 빼버리면, 명시적 width / height가 없는 SVG는 300×150픽셀(<svg>의 user-agent 기본)로 떨어진다. width/height는 있지만 viewBox는 없는 SVG는 좌표계가 그 픽셀 치수에 고정되어 스케일이 불가능해진다. 24×24 아이콘에서 viewBox를 떼고 200×200 컨테이너에 임베드하면 — 좌상단의 작은 아이콘이거나, 부모의 display 모드에 따라 늘어난 엉망진창의 결과를 본다.
viewBox 제거가 안전한 유일한 경우는, 모든 임베딩 지점을 완전히 통제하고 새 좌표계가 하류 어디선가 다시 세워질 때다 — 가령 <symbol> 요소가 자체 viewBox를 보존한 스프라이트 소스 파일, 또는 새로운 <svg viewBox=…>로 래핑하는 빌드 파이프라인. 신중히 검증된 시나리오 밖에서는 켜둔 채로 두라.
width와 height 속성도 비슷하지만 조금 덜 치명적이다. 이걸 지우면 SVG는 부모 요소로부터 렌더링 박스를 상속받는다 — 대부분의 CSS 레이아웃에선 괜찮지만 <img src="icon.svg">로 로드될 때 깨진다(내재 치수가 없는 이미지는 브라우저가 300×150으로 폴백하고, 명시적 width / height가 내재 치수를 설정하는 가장 깔끔한 방법이다).
임베딩 컨텍스트가 안전한지 확신이 없다면 둘 다 끄라. 20글자짜리 width="24" height="24"로 절감되는 몇 바이트가 깨진 아이콘 페이지의 값어치를 못 한다.
인라인 <style>와 CSS 클래스
inlineStyles + minifyStyles의 기본 동작은 가장 흔한 시나리오 — currentColor를 따라가게 하거나 인라인 fill로 덮어쓰는 — 에는 정답이다. 두 번째로 흔한 시나리오 — 외부 CSS와 함께 배포되는 SVG, 그 CSS가 클래스로 색을 입히는 — 에는 오답이다.
클래스 훅을 살려야 한다면 「모든 34개 플러그인 표시」 패널에서 inlineStyles와 minifyStyles 둘 다 꺼라. 출력은 <style> 블록과 클래스 속성을 그대로 보존한다:
<svg ... ><style>.brand{fill:#5b8def}</style><path class="brand" d="..."/></svg>
번들러 측 CSS minify가 그 <style> 블록에 한 번 더 손을 댈 수 있다는 점을 알고 있어야 한다. 즉 여기서 건너뛴 바이트가 파이프라인 끝에서 다시 회수되곤 한다. 클래스 인라인이 처음부터 끝까지 살아남는 유일한 구성은, SVGO가 파이프라인에서 유일하게 스타일을 인지하는 도구일 때뿐이다.
최적화했는데 더 커지는 경우
드물지만 SVGO 출력이 입력보다 더 커지는 경우는 있다. 가장 흔한 시나리오:
이미 minify된, 짧은 path 하나뿐인 SVG. 파일이 이미 80바이트이고 거의 <svg> 시작 태그로 차 있다면, convertPathData가 path를 다시 쓰면서 상대 좌표로 4~8바이트가 늘고, 다른 곳에서는 1바이트도 줄지 않을 수 있다. 워크벤치는 결과 카드에 빨간 +X% larger 표시로 정직하게 알려준다. 이게 보이면, 그 원본 파일은 손대지 않는 게 답이다 — 이미 최적화된 아이콘을 다시 최적화할 가치는 없다.
Multipass는 바이트 수가 더 줄지 않을 때까지 플러그인 체인을 반복한다. 바이트 비율이 경계에서 왔다갔다 한다면 multipass를 끄고 단일 패스 결과를 받아들이면 된다.
별도의 함정: 워크벤치가 보여주는 바이트 수는 CDN이 실제로 보내는 파일 크기가 아니다. 현대 CDN은 텍스트 응답에 Brotli 또는 gzip 압축을 한다. SVG 마크업은 반복이 매우 많아 와이어 사이즈는 워크벤치 표시의 30~50% 정도에 머무는 일이 많다. 그래도 마크업 사이즈로 최적화할 가치는 있다 — 지운 바이트는 파일이 도착한 뒤 파서가 안 읽어도 되는 바이트이기도 하다.
스프라이트 시트, <use>, 외부 참조
흔한 SVG 배포 패턴: 단일 스프라이트 파일에 아이콘마다 <symbol id="icon-foo">를 두고, HTML에서는 <svg><use href="sprite.svg#icon-foo"></use></svg>로 참조한다. SVGO는 역사적으로 스프라이트 소스 파일에 까다로웠다 — 스프라이트 내부에서 보면 참조되지 않은 ID가, 스프라이트 외부에서 보면 분명히 참조되고 있기 때문이다.
실용적 규칙 두 가지:
- 스프라이트 소스 파일 자체: 주 토글에서
cleanupIds를 꺼라. 스프라이트가xlink:href를 쓴다면 고급 패널에서removeUnusedNS도 꺼라. - 나중에 스프라이트로 합쳐질 개별 아이콘 SVG: 기본 설정으로 충분하다. 스프라이트 합성 도구가 어차피 ID를 다시 쓴다.
<use href="external-file.svg#id"> 같은 외부 참조는 SVGO가 보존한다 — URL도 fragment도 건드리지 않는다.
빌드 파이프라인에 통합하기
워크벤치에서 프로젝트에 맞는 플러그인 조합이 잡히면, 새 아이콘마다 일일이 수동으로 통과시키는 대신 같은 설정을 CI에 두고 싶어진다. 대표적인 두 패턴:
Vite 또는 webpack에 vite-plugin-svgo / svgo-loader 추가:
// vite.config.js
import { defineConfig } from 'vite'
import svgo from 'vite-plugin-svgo'
export default defineConfig({
plugins: [
svgo({
multipass: true,
floatPrecision: 3,
plugins: [
{
name: 'preset-default',
params: {
overrides: {
cleanupIds: false, // 런타임 CSS 훅용으로 ID 보존
removeViewBox: false, // 빌드 파이프라인에서 viewBox는 절대 빼지 않는다
},
},
},
],
}),
],
})
CI 또는 pre-commit 훅용 독립 CLI:
npm install --save-dev svgo
npx svgo --config svgo.config.js -f assets/icons -o assets/icons.optimized
조정한 설정은 저장소 루트에 svgo.config.js로 저장한다:
// svgo.config.js
export default {
multipass: true,
floatPrecision: 3,
plugins: [
{
name: 'preset-default',
params: {
overrides: {
cleanupIds: false,
removeViewBox: false,
},
},
},
],
}
워크벤치는 번들에 [email protected]을 핀해두었다. CI도 svgo@^4.0.0을 설치해 같은 overrides를 돌리면 바이트 수가 반올림 오차 안에서 일치한다. SVGO의 minor 릴리스가 가끔 한 플러그인을 더 조여 몇 바이트를 추가로 깎기 때문에, 재현이 중요한 환경에선 버전을 정확히 고정하라.
SVGOMG 등 유사 도구와의 차이
브라우저 기반 SVG 최적화 도구 생태계는 몇 가지 선택지로 수렴되어 있다. Jake Archibald가 만든 SVGOMG는 표준 구현이다 — 같은 SVGO 코어, 같은 플러그인 파이프라인, 영어 전용, 디자인 시스템 제약 없음. SVGcrop은 콘텐츠 범위로의 크롭을 다루는데, 관련 있지만 별개의 문제다. Sketch 플러그인 SVGO Compressor나 vite-plugin-svgo 같은 번들러 플러그인은 최적화 단계를 익스포트나 빌드 흐름으로 끌어올린다.
ZeroTool 최적화 도구가 의식적으로 다르게 정한 세 가지:
- SVGO 버전을 4.0.1로 고정. 막 릴리스된 상류 버전을 쓰면 SVGOMG에서 조정한 설정과 ZeroTool에서 조정한 설정이 같은 플러그인 파라미터 검증, 같은
convertPathData휴리스틱, 같은removeDeprecatedAttrs패스에 도달한다. - 34개 전부가 아니라 8개의 주 토글.
preset-default전체는 노출되어 있지만, 일상 결정의 거의 전부를 좌우하는 8개를 주 패널에 두었다.viewBox를 보존할지는 매일의 결정,<sodipodi:namedview>를 보존할지는 그렇지 않다. - 영어 · 중국어 · 일본어 · 한국어 네이티브 인터페이스. 플러그인 이름은 영어로 유지된다(어차피
svgo.config.js에 붙여 넣을 SVGO 설정 키 그 자체이므로). 그러나 모든 라벨, 상태 메시지, FAQ는 현지화돼 있다.
「단일 파일」은 의도적인 범위 제한이다. 배치 최적화, 사용자 정의 플러그인 작성, watch 모드 파이프라인, CI 통합은 SVGO 명령행의 영역이다 — 워크벤치에서 아이콘 한 개로 조정한 뒤, 그 설정을 그대로 상류로 옮겨라.
더 읽을거리
- SVGO 공식 문서 — 모든 플러그인과 파라미터의 표준 참고서.
- Sara Soueidan의 SVG 작성 · 익스포트 팁 — 디자이너가 SVGO의 일을 더 쉽게 만드는 방법.
- Chris Coyier의 인라인 SVG 가이드 — 언제 inline, 언제
<img>, 언제 sprite를 선택할 것인가. - ZeroTool의 SVG → PNG 변환기: 대상 위치가 SVG를 렌더링하지 못할 때(구식 메일 클라이언트, 일부 CMS 이미지 필드 등).
- ZeroTool의 SVG → JSX 변환기: 최적화된 아이콘을 React 컴포넌트로 변환.