포크레인 없이 못 박기 — CSS 수학 함수만으로 바 차트 그리기

9 min read
CSScalc()clamp()데이터 시각화커스텀 프로퍼티
포크레인 없이 못 박기 — CSS 수학 함수만으로 바 차트 그리기

간단한 바 차트 하나 그리겠다고 Chart.js(약 60KB)나 D3(약 90KB)를 로드하는 건 못 하나 박으려고 포크레인을 부르는 것이다. 대시보드에 매출 지표 5개를 막대로 보여주는 정도라면, CSS의 모던 수학 함수와 커스텀 프로퍼티만으로 충분하다. JavaScript는 한 줄도 필요 없다.

시맨틱 마크업이 먼저다

바 차트의 데이터는 본질적으로 순서 있는 목록이다. HTML에서 가장 자연스러운 그릇은 <ol>이다. CSS가 로드되지 않아도 데이터 자체는 읽을 수 있고, 스크린 리더도 올바르게 해석한다. 비유하면, 집의 골조가 탄탄해야 인테리어가 의미 있는 것과 같다.

bar-chart-markup.html
<figure>
  <figcaption>월별 매출 (단위: 만원)</figcaption>
  <ol class="bar-chart" role="list">
    <li style="--value: 75" aria-label="1월: 75만원">
      <span class="label">1월</span>
      <span class="value">75</span>
    </li>
    <li style="--value: 92" aria-label="2월: 92만원">
      <span class="label">2월</span>
      <span class="value">92</span>
    </li>
    <li style="--value: 68" aria-label="3월: 68만원">
      <span class="label">3월</span>
      <span class="value">68</span>
    </li>
    <li style="--value: 100" aria-label="4월: 100만원">
      <span class="label">4월</span>
      <span class="value">100</span>
    </li>
    <li style="--value: 85" aria-label="5월: 85만원">
      <span class="label">5월</span>
      <span class="value">85</span>
    </li>
  </ol>
</figure>

li--value 커스텀 프로퍼티로 데이터를 전달한다. aria-label은 스크린 리더가 "1월: 75만원"처럼 맥락과 함께 읽을 수 있도록 보장한다.

[💡 잠깐! 이 용어는?] CSS 커스텀 프로퍼티(Custom Properties): --로 시작하는 변수로, var()로 참조한다. 인라인 style 속성에서 설정하면 HTML에서 CSS로 데이터를 전달하는 파이프 역할을 한다.


수직 바 차트 — 핵심 공식 하나

calc()--value를 픽셀 높이로 변환한다. 핵심 공식은 딱 하나, calc(value / max * dimension)이다. 온도계의 수은주가 온도에 비례해 올라가는 원리와 동일하다.

vertical-bar.css
.bar-chart {
  --max: 100;
  --bar-width: 48px;
  --chart-height: 300px;
 
  display: flex;
  align-items: flex-end;
  gap: 12px;
  height: var(--chart-height);
  padding: 0;
  margin: 0;
  list-style: none;
  border-bottom: 2px solid #333;
}
 
.bar-chart li {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: var(--bar-width);
}
 
.bar-chart li::before {
  content: "";
  width: 100%;
  height: calc(var(--value) / var(--max) * var(--chart-height));
  background: #4a90d9;
  border-radius: 4px 4px 0 0;
  transition: height 0.3s ease;
}
 
.bar-chart .label {
  margin-top: 8px;
  font-size: 14px;
  color: #666;
}
 
.bar-chart .value {
  order: -1;
  font-size: 12px;
  font-weight: 600;
  color: #333;
}

값을 최대값으로 나눠 비율을 구하고, 차트 높이를 곱해 실제 픽셀로 변환한다. --max를 커스텀 프로퍼티로 빼둔 덕분에 데이터 범위가 바뀌어도 CSS 한 줄만 수정하면 된다.


clamp()로 안전장치 걸기

데이터 값이 예상 범위를 벗어나면 차트가 깨진다. 값이 0이면 바가 아예 안 보이고, 값이 최대를 초과하면 차트 영역을 뚫고 나간다. clamp()는 이런 사고를 막는 가드레일이다.

safe-bar-height.css
.bar-chart li::before {
  height: clamp(
    4px,
    calc(var(--value) / var(--max) * var(--chart-height)),
    var(--chart-height)
  );
}

[💡 잠깐! 이 용어는?] clamp(MIN, VAL, MAX): CSS 비교 함수로, max(MIN, min(VAL, MAX))와 동일하다. 값(VAL)을 최솟값(MIN)과 최댓값(MAX) 사이에 고정한다. 반응형 폰트, 간격, 차트 바 높이 등에 두루 쓰인다.

함수역할바 차트에서의 쓰임
calc()사칙연산값을 높이/너비로 변환
min()두 값 중 작은 값바 높이의 상한선
max()두 값 중 큰 값바 높이의 하한선 (0 방지)
clamp()최소·선호·최대상한+하한 동시 설정

clamp(4px, ..., var(--chart-height))를 적용하면, 값이 0이어도 최소 4px은 렌더링되고(빈 바 문제 방지), 최대값을 초과해도 차트 영역을 넘지 않는다.


수평 바 차트 — 방향만 바꾸면 된다

레이블이 긴 데이터(프로그래밍 언어 이름, 국가명 등)에는 수평 바가 더 적합하다. Grid로 레이블·바·값 영역을 깔끔하게 분리한다.

horizontal-bar.css
.bar-chart--horizontal {
  --max: 100;
  --bar-height: 32px;
  --chart-width: 100%;
 
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0;
  margin: 0;
  list-style: none;
}
 
.bar-chart--horizontal li {
  display: grid;
  grid-template-columns: 100px 1fr 48px;
  align-items: center;
  gap: 8px;
  height: var(--bar-height);
}
 
.bar-chart--horizontal li::after {
  content: "";
  height: 100%;
  width: calc(var(--value) / var(--max) * 100%);
  min-width: 4px;
  background: #4a90d9;
  border-radius: 0 4px 4px 0;
  transition: width 0.3s ease;
  grid-column: 2;
  grid-row: 1;
}
 
.bar-chart--horizontal .label {
  text-align: right;
  font-size: 14px;
  grid-column: 1;
}
 
.bar-chart--horizontal .value {
  font-size: 12px;
  font-weight: 600;
  grid-column: 3;
}

바의 너비를 calc(var(--value) / var(--max) * 100%)로 계산해 부모 너비 대비 비율로 렌더링한다. 컨테이너 크기가 바뀌면 바 너비도 자동으로 따라간다.


캐스케이드 애니메이션 추가

바가 순서대로 올라오는 진입 애니메이션은 시각적 임팩트를 확 높인다. @keyframesanimation-delay의 조합이다.

bar-animation.css
@keyframes grow-up {
  from {
    transform: scaleY(0);
  }
  to {
    transform: scaleY(1);
  }
}
 
.bar-chart li::before {
  transform-origin: bottom;
  animation: grow-up 0.6s ease-out forwards;
}
 
.bar-chart li:nth-child(1)::before { animation-delay: 0ms; }
.bar-chart li:nth-child(2)::before { animation-delay: 100ms; }
.bar-chart li:nth-child(3)::before { animation-delay: 200ms; }
.bar-chart li:nth-child(4)::before { animation-delay: 300ms; }
.bar-chart li:nth-child(5)::before { animation-delay: 400ms; }

transform-origin: bottom으로 바가 아래에서 위로 자라나게 한다. animation-delay를 100ms씩 증가시키면 도미노가 순서대로 쓰러지듯 바가 차례로 올라온다.

[💡 잠깐! 이 용어는?] transform-origin: transform이 적용되는 기준점. 기본값은 요소의 중심(50% 50%)이다. bottom으로 설정하면 아래쪽을 축으로 스케일이 적용되어, 바가 밑에서부터 성장하는 효과가 난다.


CSS 전용 vs JavaScript 라이브러리

기준CSS 전용Chart.jsD3.js
번들 크기0KB~60KB~90KB
인터랙션hover 정도툴팁, 클릭, 줌무제한
데이터 복잡도단순 1차원다차원무제한
반응형CSS 기본 지원resize 이벤트 필요직접 구현
접근성시맨틱 HTML 기반canvas (추가 작업)SVG (추가 작업)
학습 곡선낮음중간높음
적합한 경우대시보드 내 간단한 지표데이터 분석 페이지인터랙티브 시각화

단순한 바 차트, 진행 바, 비교 지표라면 CSS만으로 충분하다. 번들 크기가 0이고, 시맨틱 HTML 기반이라 접근성도 기본으로 확보된다.


마무리

CSS 수학 함수와 커스텀 프로퍼티를 조합하면 JavaScript 없이도 실용적인 바 차트가 완성된다. 핵심 공식은 calc(value / max * dimension) 하나다. clamp()로 안전장치를 걸고, 시맨틱 HTML로 접근성을 확보하면, 대시보드나 통계 페이지에서 별도 라이브러리 없이 가벼운 차트를 제공할 수 있다. 못 하나 박는 데 포크레인은 필요 없다.


참고:

관심 있을 만한 포스트

OHP 필름을 겹치듯 — conic-gradient와 attr()로 순수 CSS 파이 차트 만들기

conic-gradient, CSS 커스텀 프로퍼티, 새로운 attr() 타입 구문을 활용해 JavaScript 없이 시맨틱한 파이 차트를 구현하는 방법을 다룬다.

CSSconic-gradient

CSS @property — 커스텀 속성에 타입을 부여하는 방법

CSS @property at-rule로 커스텀 속성에 타입 정의, 상속 제어, 폴백을 추가해 렌더링 안정성과 애니메이션 가능성을 확보하는 방법을 다룬다.

CSS@property

CSS :near() — 마우스가 '가까이' 오면 반응하는 새로운 의사 클래스

CSS Working Group에 제안된 :near() 의사 클래스는 포인터 근접성을 감지해 호버 전에 UI를 활성화하는 새로운 상호작용 패턴을 연다.

CSS:near()

CSS만으로 커스텀 셀렉트 박스 — JavaScript 150줄이 사라지는 순간

Chrome 135에 도입된 appearance: base-select와 sibling-index()로 JavaScript 없이 완전한 커스텀 드롭다운을 구현하는 방법을 분석한다.

CSSbase-select

sibling-index()로 만드는 CSS 스크롤 소용돌이 — JavaScript 없이 수백 개 요소 애니메이션

CSS sibling-index()와 scroll-driven animations를 결합해 순수 CSS만으로 텍스트 보텍스 효과를 구현하는 기법을 다룬다.

CSSsibling-index

Interop 2026 — 브라우저 전쟁이 끝나고 표준 전쟁이 시작됐다

Chrome, Safari, Firefox가 합의한 20개 웹 표준 집중 영역과 프론트엔드 개발자가 주목해야 할 핵심 기능을 정리한다.

InteropCSS

CSS Stacking Context — z-index: 99999를 줬는데 왜 안 올라올까

CSS 쌓임 맥락의 생성 조건, z-index의 실제 작동 원리, 그리고 레이아웃 버그를 디버깅하는 실전 전략을 정리한다.

CSSz-index

배경색이 바뀌면 글자색도 따라가야 한다 — contrast-color() 없이 살아남는 법

브라우저 지원이 부족한 CSS contrast-color()를 color-mix(), relative color syntax, custom properties로 근사 구현하는 방법을 정리한다.

CSScontrast-color