원형 프로그레스 바가 도넛처럼 차오르는 원리

앱 로딩 화면이나 대시보드 통계 카드에서 원형 프로그레스 바가 도넛처럼 서서히 채워지는 걸 본 적 있을 것입니다. 이건 미리 만들어둔 이미지나 움직이는 GIF가 아니라, SVG 원 하나의 테두리 선을 부분적으로만 그려서 만든 결과입니다. 원리를 알면 라이브러리 없이도 직접 코드로 재현할 수 있습니다.
요약 ① 원형 프로그레스 바는 이미지가 아니라 SVG circle 요소의 테두리 선(stroke)입니다 ② stroke-dasharray로 선을 원 둘레 전체 길이만큼 긋고, stroke-dashoffset으로 보이지 않을 구간을 밀어냅니다 ③ 원 둘레(2πr)와 진행률만 알면 이 두 값을 직접 계산할 수 있습니다
원형 프로그레스 바, 왜 도넛처럼 차오르게 보일까
SVG circle 요소는 원래 fill(내부 채우기)과 stroke(테두리 선)를 따로 가집니다. fill을 투명하게 비우고 stroke만 두껍게 칠하면 도넛 모양의 링이 만들어지고, 이 링을 두 겹으로 겹쳐서 아래는 옅은 배경 원, 위는 진행률만큼만 보이는 색 원으로 구성합니다.
위쪽 원이 "진행률만큼만" 보이는 이유는 stroke-dasharray라는 속성 때문입니다. 원래는 점선을 그릴 때 쓰는 속성으로 선을 그렸다가(dash) 비웠다가(gap)를 반복하는데, 이 값을 원 둘레 전체 길이로 딱 맞춰 넣으면 dash 하나가 원 한 바퀴와 정확히 같아져서 끊김 없이 이어진 선처럼 보입니다.
여기에 stroke-dashoffset을 더하면 그 선의 시작 위치를 둘레 방향으로 밀어낼 수 있습니다. 선 길이에서 오프셋만큼을 밀어내면 그만큼이 원의 반대쪽으로 숨겨지면서 일부만 채워진 것처럼 보이고, transform으로 원을 -90도 회전시키면 시작점이 3시 방향이 아니라 12시 방향이 됩니다.
확인하는 방법 — 원 둘레 계산으로 직접 그리기
코드 몇 줄이면 라이브러리 없이 계산할 수 있습니다. 먼저 반지름(r)은 원의 지름에서 선 두께의 절반을 뺀 값입니다. 선이 원의 중심선을 따라 그려지기 때문에 선 두께만큼 안쪽 공간을 확보해야 두 원이 겹치지 않습니다.
반지름을 구했다면 원 둘레는 "2 × π × r" 공식으로 계산합니다. 예를 들어 지름 120px, 선 두께 8px이라면 반지름은 56px, 둘레는 약 351.86px이고 이 값을 stroke-dasharray에 그대로 넣으면 원 전체가 하나의 실선처럼 그려집니다.
진행률을 반영하려면 stroke-dashoffset을 "둘레 × (1 − 진행률 ÷ 100)"으로 계산합니다. 진행률 75%라면 351.86 × 0.25, 즉 약 87.96px을 오프셋으로 넣으면 됩니다. 이 두 값과 -90도 회전만 circle 태그에 넣으면 도넛형 진행률 바가 완성됩니다.
저희 도구로 확인하기. 진행률·두께·크기를 슬라이더로 바꿀 때마다 둘레와 오프셋이 자동으로 재계산되어, 값을 손으로 다시 구하지 않고도 결과와 코드를 바로 확인할 수 있습니다.
무료 · 가입 불필요 · 브라우저에서 바로 SVG·CSS 코드 생성

자주 막히는 지점
| 상황 | 원인 |
|---|---|
| 진행 방향이 반대로 돎 | stroke-dashoffset 계산 부호를 반대로 넣었거나 회전 방향을 잘못 준 경우입니다 |
| 시작점이 위쪽이 아니라 오른쪽(3시)에 있음 | circle에 transform: rotate(-90deg)를 안 준 상태입니다 |
| 배경 원과 진행 원의 두께 중심이 안 맞음 | 반지름 계산에서 선 두께의 절반을 빼지 않은 경우입니다 |
| 진행률이 바뀔 때 뚝 끊겨 보임 | stroke-dashoffset에 transition을 걸지 않아 애니메이션 없이 값만 바뀐 경우입니다 |
표에 정리된 것처럼, 어긋나는 지점은 대부분 오프셋 부호와 회전 방향 중 하나입니다. 계산식과 -90도 회전을 한 번 고정해두면 이후에는 진행률 숫자만 바꿔 넣으면 됩니다.
정리
원형 프로그레스 바는 이미지가 아니라 SVG circle의 테두리 선을 stroke-dasharray로 원 둘레 전체 길이만큼 긋고, stroke-dashoffset으로 안 보이게 할 구간을 밀어내서 만듭니다. 원 둘레(2πr)와 진행률만 있으면 두 값 모두 계산으로 구할 수 있습니다.
매번 반지름과 둘레를 손으로 계산하는 대신, 슬라이더로 값을 바꾸면서 코드가 어떻게 달라지는지 바로 확인하면 실수를 줄일 수 있습니다.
무료 · 가입 불필요 · 브라우저에서 바로 SVG·CSS 코드 생성
📖 실제 사용자들이 겪는 현실적인 사연과 트러블
실무 현장에서 원형 프로그레스 바를 처음 구현할 때 개발자와 디자이너가 가장 많이 겪는 혼란스러운 상황 세 가지를 모았습니다.
사연 1: 3시 방향에서 시작하는 헬스케어 앱 대시보드
프론트엔드 개발자 A씨는 피트니스 앱의 일일 걸음 수 목표 달성률을 보여주는 대시보드를 만들었습니다. 수식에 맞춰 SVG 원을 그렸는데, 이상하게도 게이지가 12시(정각) 방향이 아닌 오른쪽 3시(90도) 방향부터 차오르기 시작했습니다. 사용자들의 항의를 받은 뒤에야 SVG 좌표계의 기본 0도 기준점이 3시 방향이라는 것을 알게 되었고, 'transform: rotate(-90deg)'와 'transform-origin: 50% 50%'를 적용하여 12시 시작점으로 정상화할 수 있었습니다.
사연 2: 밖으로 잘려 나간 뭉툭한 테두리
주니어 UI 디자이너 B씨는 지름 100px의 원에 8px 두께의 진행률 게이지를 디자인했습니다. 이를 그대로 SVG로 구현하기 위해 viewBox를 100x100으로 잡고 반지름 $r=50\text{px}$로 설정했습니다. 결과는 처참했습니다. 선의 절반(4px)이 viewBox 바깥으로 잘려 나가면서 원의 상하좌우 가장자리가 일자로 깎인 흉측한 다각형처럼 보였습니다. 반지름은 지름의 절반에서 선 두께의 절반을 빼야 한다는 규칙($r=50-4=46\text{px}$)을 배운 뼈아픈 경험이었습니다.
사연 3: 무거운 GIF를 대체한 2KB의 기적
웹 퍼포먼스 엔지니어 C씨는 사내 데이터 분석 대시보드 화면이 너무 버벅인다는 피드백을 받았습니다. 확인해보니 각 카드마다 로딩 상태를 보여주는 화려한 애니메이션 GIF 스피너가 총 20MB나 차지하고 있었습니다. 이를 수학적 연산 기반의 가벼운 SVG 'stroke-dashoffset' 원형 게이지로 모두 교체하자, 해당 에셋 용량이 2KB로 줄어들었고 페이지 로드 시간은 60% 단축되었으며 클라이언트 노트북의 팬 소음(CPU 스파이크) 문제도 깔끔하게 해결되었습니다.
🔬 핵심 기술 메커니즘 — SVG와 CSS의 수학적 원리
단순해 보이는 도넛형 그래프 이면에는 정밀한 기하학과 CSS 물리가 숨어 있습니다.
SVG 원의 기하학과 테두리 수학 (Geometry & Stroke Math)
- 전체 SVG 규격: $W \times H$ (예: $120 \times 120\text{px}$)
- 선 두께(Stroke Width): $S$ (예: $8\text{px}$)
- 안전 반지름(Radius): 선이 중심을 기준으로 양쪽으로 퍼지므로, $r = \frac{W}{2} - \frac{S}{2} = 60 - 4 = 56\text{px}$
- 원 둘레(Circumference): $C = 2\pi r \approx 2 \times 3.14159265 \times 56 \approx 351.86\text{px}$
CSS Dash 속성의 물리 엔진 (Physics)
- 'stroke-dasharray: C C': 점선의 한 조각(dash) 길이를 원 전체 둘레와 동일하게 설정하여, 실선처럼 보이게 만듭니다.
- 'stroke-dashoffset: C \times (1 - P / 100)': $P$가 진행률(%)일 때, 이 값만큼 실선의 시작 위치를 원의 둘레를 따라 밀어냅니다. 밀려난 부분은 보이지 않으므로 원하는 퍼센트만큼만 색이 칠해진 효과를 냅니다.
좌표계 회전과 안티앨리어싱 (Rotation & Anti-aliasing)
- SVG의 기본 0도 기준점은 기하학 관례에 따라 3시 방향($x=r, y=0$)입니다.
- 이를 12시 방향(상단)에서 시작하게 하려면, CSS를 통해 'transform: rotate(-90deg);'와 회전 중심축 'transform-origin: center;'를 지정해야 합니다.
- 부드럽게 차오르는 애니메이션은 'transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1);'로 구현하며, 선 끝단을 둥글게 마감하기 위해 'stroke-linecap: round;'를 사용합니다.
📊 원형 UI 구현 방식 비교 분석표
원형 프로그레스 바를 구현하는 여러 기술 간의 특징을 비교해 보았습니다.
| 비교 항목 | 순수 SVG + CSS (권장) | HTML5 Canvas 'arc()' | CSS 'conic-gradient' / 마스크 | Animated GIF / Lottie |
|---|---|---|---|---|
| 파일 용량 | 매우 작음 (~2KB) | 작음 | 작음 | 큼 (수십 KB ~ MB 단위) |
| 벡터 확장성 | 무한 스케일링, 깨짐 없음 | 해상도(DPI)에 따라 깨질 수 있음 | 무한 스케일링 | GIF 픽셀 깨짐 / Lottie 무관 |
| CPU/GPU 부하 | 매우 낮음 (GPU 가속 용이) | 스크립트 연산에 의한 CPU 부하 발생 | 낮음 | GPU 메모리 점유 및 CPU 렌더링 부하 큼 |
| CSS 애니메이션 | 'stroke-dashoffset'으로 부드럽게 적용 | JS 'requestAnimationFrame' 필요 | '@property' 필요 (구형 브라우저 제약) | 불가능 / 라이브러리에 종속 |
| 브라우저 호환성 | IE 포함 거의 모든 브라우저 100% 호환 | 100% 호환 | 최신 브라우저 위주 (부분 지원) | IE 호환 (Lottie는 최신 환경 한정) |
⚠️ 원형 프로그레스 SVG 제작 시 흔한 실수 5가지
- 반지름에서 선 두께의 절반을 빼지 않는 실수: SVG 테두리는 패스의 중심선을 따라 양방향으로 뻗어나갑니다. 이를 빼지 않으면 바깥쪽 절반이 viewBox 밖으로 밀려나 잘리게 됩니다.
- -90도 회전 시 'transform-origin: center' 누락: 회전 중심축을 설정하지 않으면 SVG의 기본 0,0 좌표(좌상단)를 기준으로 원이 화면 밖으로 날아가 버립니다.
- 'stroke-linecap: round' 생략: 진행률 게이지 끝이 칼로 자른 듯 평평하게 끝나 투박한 구시대 UI처럼 보입니다.
- 저정밀도 원주율($\pi = 3.14$) 하드코딩 사용: 둘레 값을 부정확하게 계산하면 진행률 100% 도달 시 시작점과 끝점 사이에 미세한 틈(Gap)이 생기거나 겹치게 됩니다. JS의 'Math.PI'를 사용하세요.
- CSS 'transition' 누락: 데이터 변경 시 게이지가 뚝뚝 끊기며 순간이동을 합니다. 오프셋 속성에 적절한 트랜지션을 주어야 유려한 피드백이 발생합니다.
💡 5단계 실패 없는 원형 프로그레스 바 워크플로우
- 규격 및 뷰박스 정의: 전체 사이즈(예: 100x100)를 결정하고 SVG 'viewBox="0 0 100 100"'을 설정합니다.
- 안전 반지름 도출: 선 두께(예: 10)를 정한 뒤, 수식 $r = (D - \text{stroke}) / 2$ 에 따라 $r = 45$ 를 구합니다.
- 둘레 길이 연산: $C = 2\pi r$ 공식을 사용해 총 둘레 길이를 계산합니다. ($2 \times 3.14159 \times 45 = 282.74$)
- 배경 트랙 및 인디케이터 배치: 동일한 크기의 원 두 개를 겹치되, 아래는 옅은 색의 트랙으로, 위는 진행률 색상의 인디케이터로 칠합니다.
- CSS 트랜지션 및 오프셋 적용: 퍼센티지 수식에 따라 'stroke-dashoffset'을 계산하여 인라인 스타일로 주입하고 트랜지션으로 애니메이션을 완성합니다.
🔍 전문가의 SVG 애니메이션 팁 7선
- 둥근 끝처리: 모던하고 세련된 UI를 위해 항상 'stroke-linecap: round'를 습관화하세요.
- 그라디언트 칠하기: 단색 대신 '
'를 선언하고 'stroke="url(#gradient)"'로 지정하여 화려한 효과를 연출하세요. - 하드웨어 가속: 애니메이션 프레임 드랍을 막기 위해 게이지 원 요소에 'will-change: stroke-dashoffset'을 부여하세요.
- 100% 상태의 안정성: 100% 일 때는 'stroke-dashoffset: 0'으로 떨어지도록 보장하여 약간의 오차가 발생하지 않게 만드세요.
- 눈금선(미터기) 스타일: 'stroke-dasharray'를 둘레 길이와 갭으로 분할(예: '5 10')하면 연속된 실선 대신 자동차 계기판 같은 점선 게이지를 만들 수 있습니다.
- 반원 형태 게이지: 둘레를 절반으로 쪼개고 하단 부분을 자른 뒤, 뷰박스와 오프셋 수식을 반원으로 맞추면 속도계 같은 UI가 완성됩니다.
- 생성기 적극 활용: 오차 없는 렌더링을 확인하려면 매번 손으로 계산하기보다 웹 생성기 도구를 활용해 실시간 슬라이더로 테스트하세요.
🎯 기술 결정 트리: 어떤 방식이 적합할까?
- 데이터 시각화 등 인터랙션이 많고 CSS 애니메이션이 핵심인가? ➔ SVG 'stroke-dashoffset' 방식 (추천)
- 입자 효과(Particle)나 초당 60프레임 이상의 픽셀 조작이 필요한가? ➔ HTML5 Canvas 렌더링
- IE 지원이 필요 없고 코드를 극단적으로 줄여야 하는 정적 그래프인가? ➔ CSS 'conic-gradient' 방식
- 개발자 없이 디자이너 단독으로 복잡한 모션 그래픽을 삽입해야 하는가? ➔ Lottie / 애니메이션 GIF
📑 핵심 용어 사전 (Terminology Cheatsheet)
- SVG (Scalable Vector Graphics): 수학적 방정식으로 그려져 아무리 확대해도 깨지지 않는 2차원 벡터 그래픽 포맷.
- stroke-dasharray: 선의 길이와 간격을 지정하여 실선을 점선 조각으로 나누는 속성. 여기서는 원 둘레를 통째로 하나의 점선으로 만듭니다.
- stroke-dashoffset: 점선이 시작되는 위치를 밀어내는 오프셋 속성. 진행률에 역비례하게 설정하여 남은 빈 구간을 연출합니다.
- 둘레(Circumference, 2πr): 원의 테두리 전체 길이. 프로그레스 바의 100% 길이가 됩니다.
- viewBox: SVG 캔버스의 논리적 좌표계 영역.
- stroke-linecap: round: 선의 양 끝을 둥글게 마감하는 SVG 속성.
- transform-origin: 회전이나 크기 조절 시 기준이 되는 중심축.
- 하드웨어 가속(will-change): 브라우저가 특정 요소의 애니메이션 연산을 GPU로 넘겨 부드럽게 렌더링하도록 힌트를 주는 CSS 속성.
자주 묻는 질문
Q1. 왜 굳이 지름에서 선 두께의 절반을 빼서 반지름을 구해야 하나요?
A1. SVG에서 선(Stroke)은 가상의 경로(Path)를 중심으로 양쪽으로 동일하게 두꺼워집니다. 선 두께가 10px이라면 중심선 안쪽으로 5px, 바깥쪽으로 5px이 자라납니다. 바깥쪽으로 자라난 5px이 캔버스 밖으로 나가는 것을 막기 위해, 원 자체의 반지름을 5px 줄여 안쪽으로 욱여넣는 것입니다.
Q2. 원형 경로를 따라 여러 색상이 부드럽게 이어지는 그라디언트를 넣을 수 있나요?
A2. 네, SVG 내부에 '
Q3. 시계 반대 방향(Counter-clockwise)으로 게이지가 줄어들게 하려면 어떻게 하나요?
A3. 게이지가 차오르는 대신 점점 비워지도록 하려면 오프셋 계산 수식을 뒤집거나, 원을 -90도가 아닌 반대 방향으로 회전시키면서 오프셋 방향을 조절하면 됩니다. 가장 간단한 방법은 'transform: scaleX(-1) rotate(-90deg)' 처럼 좌우를 반전시켜 역방향 렌더링을 유도하는 것입니다.
