SVG를 알아보자
2025-02-01이전 회사에서 도넛 차트와 막대 차트를 구현할 일이 있었습니다. 처음에는 라이브러리를 사용하려고 했지만 요청받은 디자인을 반영할 수 없어서, SVG를 사용해 직접 구현했었는데요.
그 당시에는 만들기 급급해서 SVG의 자세한 내용을 분석하지 못했던 아쉬움이 있어서, 이번 기회에 자세히 정리해보려고 합니다.
1. SVG 기본 구조
SVG(Scalable Vector Graphics)는 웹에서 벡터 그래픽을 표현하기 위한 도구입니다.
주요 특징:
- 확대/축소 시에도 깨짐이 없습니다
- XML 형식으로 작성됩니다
- HTML 문서에서
<svg>태그를 사용하여 정의할 수 있습니다
SVG 안에는 다양한 그래픽 요소를 추가할 수 있으며, 주요 태그로는 <circle>, <rect>, <line>, <path> 등이 있습니다.
예시: 간단한 원 그리기
<svg width="200" height="200">
<circle
cx="100"
cy="100"
r="80"
fill="skyblue"
stroke="black"
stroke-width="2"
/>
</svg>
출력 결과로는 파란색 내부와 검은색 외곽선을 가진 원이 그려집니다.
2. SVG 기본 요소와 속성
SVG에서는 다양한 도형을 그릴 수 있습니다.
1) <rect> - 사각형
<rect>를 사용하면 사각형을 만들 수 있습니다.
<svg width="200" height="200">
<rect
x="20"
y="20"
width="150"
height="100"
fill="lightgreen"
stroke="black"
stroke-width="2"
rx="10"
ry="10"
/>
</svg>
주요 속성:
x,y: 사각형의 시작 위치width,height: 사각형의 크기rx,ry: 모서리의 둥근 정도
2) <line> - 선
<line>을 사용하면 선을 그릴 수 있습니다.
<svg width="200" height="200">
<line x1="10" y1="10" x2="190" y2="190" stroke="green" stroke-width="4" />
</svg>
주요 속성:
x1,y1: 시작점 좌표x2,y2: 끝점 좌표
3) <path> - 복잡한 도형
<path>를 사용하면 원하는 복잡한 도형을 그릴 수 있습니다.
<svg width="200" height="200">
<path
d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80"
fill="none"
stroke="red"
stroke-width="2"
/>
</svg>
주요 경로 명령어:
| 명령어 | 설명 |
|---|---|
| M x y | 시작 위치 이동 (Move to) |
| L x y | 직선 그리기 (Line to) |
| C x1 y1, x2 y2, x y | 큐빅 베지어 곡선 (두 개의 제어점과 끝점을 이용한 곡선) |
| S x2 y2, x y | 부드러운 곡선 (이전 곡선의 연장) |
3. SVG 애니메이션
SVG에서는 stroke-dasharray와 stroke-dashoffset 속성을 사용해서 선이 그려지는 애니메이션을 표현할 수 있습니다.
stroke-dasharray
선을 점선으로 표현할 때, 선과 빈 공간의 길이를 지정합니다.
예시: stroke-dasharray: 5, 10
- 선이 5px
- 빈 공간이 10px
- 이 패턴이 반복됩니다
stroke-dashoffset
선을 점차적으로 그려지는 효과를 구현할 수 있습니다.
<svg width="200" height="200">
<circle cx="100" cy="100" r="80" fill="none" stroke="red" stroke-width="4" stroke-dasharray="502" stroke-dashoffset="502">
<style>
circle {
animation: draw 3s linear infinite;
}
@keyframes draw {
from {
stroke-dashoffset: 502;
}
to {
stroke-dashoffset: 0;
}
}
</style>
</svg>
애니메이션 동작 원리
stroke-dasharray="502": 원의 둘레만큼 점선 패턴을 설정합니다stroke-dashoffset="502": 처음에는 완전히 숨깁니다- 애니메이션으로 offset을 0으로 변경하면서 점차 그려집니다
4. 실제로 구현했던 도넛 차트
도넛 차트는 stroke-dasharray와 stroke-dashoffset을 조합해서 만듭니다. 핵심 아이디어는 원 둘레를 퍼센트에 따라 분할하는 것입니다.
계산 원리
원의 둘레는 2πr입니다. 예를 들어 반지름이 80이면 둘레는 약 502입니다.
각 세그먼트는 (비율 × 둘레) 길이의 선분, 나머지는 투명 처리하면 원하는 조각이 그려집니다.
type DonutSegment = {
value: number; // 전체에서 차지하는 퍼센트 (0~100)
color: string;
};
type DonutChartProps = {
data: DonutSegment[];
size?: number;
strokeWidth?: number;
};
const DonutChart = ({ data, size = 200, strokeWidth = 30 }: DonutChartProps) => {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const center = size / 2;
let accumulatedOffset = 0;
return (
<svg width={size} height={size}>
{data.map((segment, i) => {
const dash = (segment.value / 100) * circumference;
// 이전 세그먼트만큼 오프셋을 당겨서 시작 위치를 조정
const offset = circumference - accumulatedOffset;
accumulatedOffset += dash;
return (
<circle
key={i}
cx={center}
cy={center}
r={radius}
fill="none"
stroke={segment.color}
strokeWidth={strokeWidth}
strokeDasharray={`${dash} ${circumference - dash}`}
strokeDashoffset={offset}
// SVG 원의 0도는 3시 방향 → 12시 방향에서 시작하려면 -90도 회전
style={{ transform: "rotate(-90deg)", transformOrigin: "center" }}
/>
);
})}
</svg>
);
};
삽질 포인트
rotate(-90deg)를 꼭 써야 하는 이유: SVG에서 stroke-dashoffset은 3시 방향(오른쪽)에서 시작합니다. 차트는 보통 12시 방향에서 시작하기를 기대하므로, -90도 회전이 필요합니다. 처음에 이 부분을 몰라서 차트가 옆으로 누운 채로 나와서 당황했습니다.
Safari에서 transformOrigin 버그: style={{ transformOrigin: "center" }}가 Safari에서 작동하지 않을 수 있습니다. 이 경우 transform-box: fill-box를 CSS에 추가하거나, transform="rotate(-90, ${center}, ${center})"처럼 SVG 속성으로 처리하는 게 안전합니다.
5. 실제로 구현했던 막대 차트
막대 차트의 핵심 함정은 SVG 좌표계가 위에서 아래 방향이라는 점입니다. 화면 좌표와 반대라서 처음에 계산이 직관적이지 않습니다.
계산 원리
막대 높이를 (값 / 최댓값) × 차트 높이로 구하고, y 좌표는 차트 높이 - 막대 높이로 계산합니다.
type BarSegment = {
label: string;
value: number;
color: string;
};
type BarChartProps = {
data: BarSegment[];
width?: number;
height?: number;
padding?: number;
};
const BarChart = ({ data, width = 400, height = 300, padding = 8 }: BarChartProps) => {
const maxValue = Math.max(...data.map((d) => d.value));
const totalBars = data.length;
const barWidth = (width / totalBars) * 0.6;
const gap = (width / totalBars) * 0.4;
return (
<svg width={width} height={height}>
{data.map((item, i) => {
const barHeight = (item.value / maxValue) * (height - padding);
// SVG y축은 위에서 아래 방향이므로 아래서 위로 막대를 그리려면 이렇게 계산
const x = i * (barWidth + gap);
const y = height - barHeight;
return (
<g key={i}>
<rect x={x} y={y} width={barWidth} height={barHeight} fill={item.color} rx={4} />
<text
x={x + barWidth / 2}
y={height}
textAnchor="middle"
fontSize={12}
fill="currentColor"
>
{item.label}
</text>
</g>
);
})}
</svg>
);
};
삽질 포인트
y 좌표 계산이 역방향: 막대가 아래서 위로 자라야 하는데, SVG의 y는 "막대의 위쪽 끝" 좌표입니다. 그래서 y = height - barHeight로 계산해야 합니다. 처음에 y = barHeight로 하면 막대가 위에서 아래로 그려져서 뒤집힌 차트가 됩니다.
텍스트 레이블 위치: <text> 태그의 y 좌표도 마찬가지입니다. y={height}로 설정해야 차트 하단에 레이블이 붙습니다.
정리
SVG의 주요 장점:
- 확대/축소 시에도 깨지지 않습니다
- CSS와 JavaScript로 쉽게 제어할 수 있습니다
- 애니메이션 구현이 간단합니다
- 파일 크기가 작습니다
라이브러리 없이 직접 구현하면 디자인 제약 없이 원하는 대로 만들 수 있다는 장점이 있지만, SVG 좌표계와 stroke-dashoffset 계산 방식은 한 번 제대로 이해해두지 않으면 매번 시행착오를 반복하게 됩니다.