OHP 필름을 겹치듯 — conic-gradient와 attr()로 순수 CSS 파이 차트 만들기
파이 차트를 그리겠다고 Chart.js나 D3를 꺼내드는 건 자연스러운 일이다. 하지만 CSS의 conic-gradient()가 등장한 이후, "순수 CSS로도 가능하지 않을까?"라는 질문이 계속 나온다. 가능하다. 다만 예쁘기만 하고 의미 없는 원이 아니라, 시맨틱하고 접근 가능한 차트를 만들려면 몇 가지 함정을 피해야 한다.
단순한 접근의 함정
가장 직관적인 방법은 하나의 div에 conic-gradient()를 때려 넣는 것이다.
.pie {
width: 200px;
height: 200px;
border-radius: 50%;
background: conic-gradient(
#ff6666 0% 35%,
#4fff66 35% 60%,
#66ffff 60% 85%,
#b366ff 85% 100%
);
}이건 비유하면 포스터 한 장에 데이터를 그려놓는 것이다. 사람 눈에는 보이지만, 스크린 리더는 이것이 "초콜릿 35%, 젤리 25%"라는 데이터를 담고 있다는 사실을 전혀 알 수 없다. CSS 그라디언트는 본질적으로 이미지이고, 이미지에는 시맨틱 정보가 없다.
문제가 하나 더 있다. 슬라이스가 늘어날수록 conic-gradient() 안의 색상 정지점을 일일이 수동 계산해야 한다. 이전 슬라이스의 끝을 다음 슬라이스의 시작으로 넘겨야 하니, 유지보수가 악몽이 된다.
[💡 잠깐! 이 용어는?]
conic-gradient(): 원뿔형 그라디언트. 중심점을 기준으로 시계 방향으로 색상이 전환되며, 파이 차트나 도넛 차트를 만드는 데 활용된다. linear-gradient()가 직선이라면, conic-gradient()는 원이다.
시맨틱 HTML 설계 — 데이터는 목록이다
파이 차트의 데이터는 "항목 A가 35%, 항목 B가 25%…"처럼 각 항목의 비중을 나열하는 것이니, HTML에서도 목록으로 표현하는 것이 자연스럽다. 구조가 탄탄한 집에 인테리어를 입히는 것이 맞는 순서다.
<figure>
<figcaption>지난달 사탕 판매 비율</figcaption>
<ul class="pie-chart" role="list">
<li data-percentage="35" data-color="#ff6666" aria-label="초콜릿 35%">
<strong>초콜릿</strong>
</li>
<li data-percentage="25" data-color="#4fff66" aria-label="젤리 25%">
<strong>젤리</strong>
</li>
<li data-percentage="25" data-color="#66ffff" aria-label="하드캔디 25%">
<strong>하드캔디</strong>
</li>
<li data-percentage="15" data-color="#b366ff" aria-label="풍선껌 15%">
<strong>풍선껌</strong>
</li>
</ul>
</figure>figure와 figcaption으로 차트의 맥락을 제공하고, ul > li로 각 데이터 항목을 표현한다. data-percentage와 data-color는 CSS에서 시각적 렌더링에 사용하고, aria-label은 스크린 리더가 각 항목의 의미를 전달할 수 있게 보장한다.
핵심 트릭 — 투명 필름을 겹쳐 쌓기
여기서 핵심 아이디어는 각 li가 자기 슬라이스만 그리고, 전부 같은 위치에 겹쳐놓는 것이다. 마치 투명한 OHP 필름 여러 장을 겹치는 것과 비슷하다. 각 필름에는 파이의 한 조각만 그려져 있고, 전부 합치면 완성된 원이 나타난다.
.pie-chart {
--radius: 100px;
--size: calc(var(--radius) * 2);
position: relative;
width: var(--size);
height: var(--size);
list-style: none;
padding: 0;
margin: 0;
}
.pie-chart li {
position: absolute;
inset: 0;
border-radius: 50%;
}모든 li를 position: absolute로 같은 위치에 쌓고, border-radius: 50%로 원형을 만든다. 이제 각 슬라이스에 conic-gradient()를 적용하면 된다.
[💡 잠깐! 이 용어는?]
attr() 타입 구문: CSS attr() 함수의 확장 기능으로, HTML 속성 값을 특정 타입(number, length 등)으로 파싱해 CSS 수학 연산에 사용할 수 있게 한다. attr(data-percentage number)처럼 사용한다.
attr()로 데이터와 CSS 연결
CSS의 새로운 attr() 타입 구문으로 data-percentage 값을 직접 숫자로 파싱할 수 있다. 각 슬라이스의 시작 각도는 인라인 스타일로 누적값을 전달하는 방식이 현실적이다.
<li data-percentage="35" data-color="#ff6666" style="--start: 0" aria-label="초콜릿 35%">
<strong>초콜릿</strong>
</li>
<li data-percentage="25" data-color="#4fff66" style="--start: 35" aria-label="젤리 25%">
<strong>젤리</strong>
</li>
<li data-percentage="25" data-color="#66ffff" style="--start: 60" aria-label="하드캔디 25%">
<strong>하드캔디</strong>
</li>
<li data-percentage="15" data-color="#b366ff" style="--start: 75" aria-label="풍선껌 15%">
<strong>풍선껌</strong>
</li>--start는 이전 슬라이스들의 퍼센트 합계다. 초콜릿은 0에서 시작, 젤리는 35에서 시작(초콜릿 35%), 하드캔디는 60에서 시작(35+25%), 이런 식이다. 비유하면, 릴레이 경주에서 이전 주자가 끝난 지점에서 다음 주자가 출발하는 것과 같다.
.pie-chart li {
--pct: attr(data-percentage number, 0);
--col: attr(data-color type(<color>), gray);
--from: calc(var(--start) * 3.6deg);
--to: calc((var(--start) + var(--pct)) * 3.6deg);
background: conic-gradient(
transparent 0deg var(--from),
var(--col) var(--from) var(--to),
transparent var(--to) 360deg
);
}3.6deg은 360 / 100이다. 퍼센트 1%가 3.6도에 해당하므로, 퍼센트 값에 3.6을 곱하면 각도로 변환된다. 각 슬라이스는 자기 영역만 색칠하고 나머지는 transparent로 비워두기 때문에, 겹쳐도 간섭 없이 완전한 원이 완성된다.
[💡 잠깐! 이 용어는?]
색상 정지점(Color Stop): 그라디언트에서 특정 색상이 시작되거나 끝나는 위치. conic-gradient(red 0deg 90deg, blue 90deg 180deg)에서 0deg, 90deg, 180deg가 각각 색상 정지점이다.
구현 방식 비교
| 기준 | 단일 conic-gradient | 슬라이스 겹치기 | JavaScript 라이브러리 |
|---|---|---|---|
| 시맨틱 HTML | 불가 | 가능 (ul > li) | 라이브러리 의존 |
| 접근성 | 없음 | aria-label 활용 | 라이브러리 의존 |
| 유지보수 | 정지점 수동 계산 | data 속성만 변경 | API 기반 |
| JavaScript 의존 | 없음 | 없음 | 필수 |
| 브라우저 지원 | 우수 | Chromium 중심 | 우수 |
| 번들 크기 | 0KB | 0KB | 30~200KB |
attr() 타입 구문이 아직 Chromium 기반 브라우저에서만 지원되므로, 프로덕션에서는 폴백이 필요하다.
[💡 잠깐! 이 용어는?]
폴백(Fallback): 특정 기능이 지원되지 않는 환경에서 대체 동작을 제공하는 것. @supports로 attr() 타입 구문 지원 여부를 감지하고, 미지원 시 JavaScript 기반 차트를 로드하는 식으로 구현한다.
마무리
CSS conic-gradient()와 attr() 타입 구문을 조합하면 JavaScript 없이도 시맨틱하고 접근 가능한 파이 차트를 만들 수 있다. 핵심은 단일 그라디언트에 모든 데이터를 우겨넣는 게 아니라, 각 슬라이스를 독립적인 리스트 아이템으로 분리한 뒤 OHP 필름처럼 겹치는 것이다. attr() 타입 구문의 브라우저 지원이 확대되면, 간단한 데이터 시각화에 별도 라이브러리가 필요 없는 날이 올 것이다. 지금 당장은 Chromium 한정이지만, 기법을 익혀두면 충분히 가치가 있다.
참고:
- CSS-Tricks: https://css-tricks.com/trying-to-make-the-perfect-pie-chart-in-css/
- MDN conic-gradient(): https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/conic-gradient
관심 있을 만한 포스트
배경색이 바뀌면 글자색도 따라가야 한다 — contrast-color() 없이 살아남는 법
브라우저 지원이 부족한 CSS contrast-color()를 color-mix(), relative color syntax, custom properties로 근사 구현하는 방법을 정리한다.
포크레인 없이 못 박기 — CSS 수학 함수만으로 바 차트 그리기
CSS 수학 함수와 커스텀 프로퍼티, 시맨틱 HTML을 조합해 JavaScript 없이 반응형 바 차트를 구현하는 방법을 다룬다.
CSS @property — 커스텀 속성에 타입을 부여하는 방법
CSS @property at-rule로 커스텀 속성에 타입 정의, 상속 제어, 폴백을 추가해 렌더링 안정성과 애니메이션 가능성을 확보하는 방법을 다룬다.
CSS :near() — 마우스가 '가까이' 오면 반응하는 새로운 의사 클래스
CSS Working Group에 제안된 :near() 의사 클래스는 포인터 근접성을 감지해 호버 전에 UI를 활성화하는 새로운 상호작용 패턴을 연다.
CSS만으로 커스텀 셀렉트 박스 — JavaScript 150줄이 사라지는 순간
Chrome 135에 도입된 appearance: base-select와 sibling-index()로 JavaScript 없이 완전한 커스텀 드롭다운을 구현하는 방법을 분석한다.
sibling-index()로 만드는 CSS 스크롤 소용돌이 — JavaScript 없이 수백 개 요소 애니메이션
CSS sibling-index()와 scroll-driven animations를 결합해 순수 CSS만으로 텍스트 보텍스 효과를 구현하는 기법을 다룬다.
Interop 2026 — 브라우저 전쟁이 끝나고 표준 전쟁이 시작됐다
Chrome, Safari, Firefox가 합의한 20개 웹 표준 집중 영역과 프론트엔드 개발자가 주목해야 할 핵심 기능을 정리한다.
CSS Stacking Context — z-index: 99999를 줬는데 왜 안 올라올까
CSS 쌓임 맥락의 생성 조건, z-index의 실제 작동 원리, 그리고 레이아웃 버그를 디버깅하는 실전 전략을 정리한다.