back

Posts

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-dasharraystroke-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>

애니메이션 동작 원리

  1. stroke-dasharray="502": 원의 둘레만큼 점선 패턴을 설정합니다
  2. stroke-dashoffset="502": 처음에는 완전히 숨깁니다
  3. 애니메이션으로 offset을 0으로 변경하면서 점차 그려집니다

4. 실제로 구현했던 도넛 차트

도넛 차트는 stroke-dasharraystroke-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 계산 방식은 한 번 제대로 이해해두지 않으면 매번 시행착오를 반복하게 됩니다.