GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

주식 앱 ui 실시간 시세판 9종 — API 키 없이

주식 앱 ui 의 시세판은 사람이 새로고침하지 않아도 숫자가 저절로 바뀌는 화면입니다. 값이 오르면 초록으로, 내리면 빨강으로 한 번 번쩍인 뒤 잠잠해지는 그 장면을 아홉 가지로 만들었고, 증권사 계정도 유료 데이터도 없이 파일만 열면 그대로 돕니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

순서는 인기순이 아니라 시세 한 건이 화면에 닿는 순서입니다. 값 하나가 셀 안에서 번쩍이고(01), 그런 값들이 띠를 이뤄 흐르고(02), 시간이 쌓여 선이 되고(03), 선이 봉으로 굳습니다(04). 다음 셋은 종목 하나가 아니라 판 전체를 봅니다 — 사려는 쪽과 팔려는 쪽의 두께(05), 보유 종목 전부의 색(06), 평가손익 한 덩어리(07). 마지막 둘은 사람에게 말을 거는 쪽이라, 정해 둔 선을 넘었다고 알리고(08), 지금 이 숫자를 믿어도 되는지 알려 줍니다(09). 값이 들어오는 통로는 아홉 개 모두 같습니다: 시드를 받는 작은 난수 발생기가 정해진 간격마다 다음 값을 만들고, 그 자리에 나중에 실제 API 응답을 끼워 넣으면 화면 코드는 손댈 곳이 없습니다. 주식 차트 ui 를 처음부터 그려 내는 쪽은 SVG 그래프 그리기 9종에, 이미 그려진 그래프를 마우스로 만지는 쪽은 대시보드 차트 인터랙션 9종에 있습니다 — 이 편은 그 둘과 갈라져 값이 계속 들어올 때 화면이 어떻게 변하는가 한 축만 다룹니다. 초 단위가 아니라 하루 단위로 값을 쌓아 보여 주는 쪽은 같은 dashboard 기둥의 습관 트래커 UI가 맡고 있어, 갱신 주기만 다른 같은 문제를 정반대 끝에서 풉니다.

01오르면 초록 내리면 빨강, 플래시 시세 셀

무엇이 보이나

관심 종목 네 줄 가운데 값이 바뀐 한 줄만 배경이 한 프레임에 켜졌다가 600ms 동안 스스로 빠집니다. 가격과 등락률은 같은 칸에 두 장을 겹쳐 두고 컷으로 갈아 끼우기 때문에, 자릿수가 달라져도 오른쪽 끝이 제자리에 섭니다.

오르면 초록 내리면 빨강, 플래시 시세 셀
새 창에서 열기

01 · 오르면 초록 내리면 빨강, 플래시 시세 셀 — 측정 누적면적 19.33% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

steps(1, end)tabular-numssetInterval

코드에서 볼 곳

2초 루프에서 움직인 프레임 9/23 · 누적면적 19.33% 를 기록했고, 관심 종목 목록이나 실시간 주문 체결 화면의 현재가 칸에 씁니다.

// 이전값·현재값을 같은 칸에 겹친다 — 컷 교체라 폭이 달라도 자리가 흔들리지 않는다
.pf__priceWrap, .pf__chgWrap { display: grid; justify-items: end; }
.pf__price, .pf__chg { grid-area: 1 / 1; font-variant-numeric: tabular-nums; }
.pf__price {
  font-weight: 800; font-size: clamp(12px, 3.4vw, 15px); line-height: 1.5; color: $subject-cream;
}
.pf__chg {
  font-weight: 700; font-size: clamp(10px, 2.6vw, 11px); line-height: 1.5; min-width: 62px;
}
.pf__chg.is-up { color: $tick-up; }
.pf__chg.is-down { color: $tick-down; }
// 실사용(정적 경로)에서는 현재값만 보인다 — 이전값은 자리만 지키고 안 보인다
.pf:not(.is-demo) .pf__price--prev, .pf:not(.is-demo) .pf__chg--prev { opacity: 0; }

// 실사용 플래시 — JS 가 붙인 is-up / is-down 이 한 프레임에 배경을 켜고 스스로 꺼진다.
// steps(1, end) 로 켜는 건 값 교체 쪽이고, 배경은 켜진 값에서 시작해 이어서 빠진다
.pf__row.is-up { animation: pfFlashUp 600ms $easing 1; }
.pf__row.is-down { animation: pfFlashDown 600ms $easing 1; }
@keyframes pfFlashUp { from { background-color: $flash-up; } to { background-color: transparent; } }
@keyframes pfFlashDown { from { background-color: $flash-down; } to { background-color: transparent; } }

02가로로 흐르는 티커 테이프

무엇이 보이나

같은 종목 줄을 두 벌 이어 붙인 트랙이 왼쪽으로 끊김 없이 흐르고, 포인터를 올리거나 키보드로 테이프에 들어가면 흐름이 그 자리에 섭니다. 흐르는 동안에도 700ms 마다 한 칸씩 값이 갈리고, 새 종목이 상장하면 두 벌 양쪽에 같은 칸이 함께 덧붙습니다.

가로로 흐르는 티커 테이프
새 창에서 열기

02 · 가로로 흐르는 티커 테이프 — 측정 누적면적 2.469% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateXanimation-play-statemargin-right

간격을 왜 margin-right 로 주나

24프레임 전부, 그러니까 움직인 프레임 23/23 에 변화가 잡혔고 누적면적은 2.469% 로 얇으며, 서비스 상단 고정 시세 띠나 로비 모니터처럼 늘 켜져 있는 자리에 씁니다.

.tt__item {
  display: inline-flex; align-items: center; flex-shrink: 0; white-space: nowrap;
  margin-right: $sp-6;   // 트랙의 gap 대신 — 한 벌의 폭이 (칸 + 간격) × N 으로 딱 떨어진다
}
.tt__sym {
  margin-right: $sp-2; font-weight: 700; font-size: clamp(10px, 2.8vw, 12px);
  color: $tick-mute; letter-spacing: .04em;
}
.tt__price {
  margin-right: $sp-2; font-weight: 800; font-size: clamp(11px, 3.2vw, 14px); line-height: 1.5;
  color: $color; font-variant-numeric: tabular-nums;
}
.tt__chg { font-weight: 700; font-size: clamp(10px, 2.6vw, 11px); line-height: 1.5; font-variant-numeric: tabular-nums; }
.tt__chg.is-up { color: $tick-up; }
.tt__chg.is-down { color: $tick-down; }

@keyframes ttSlide {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

// 포인터·터치·키보드 어느 쪽으로 들어와도 테이프가 선다 — 읽는 중에 글자가 지나가지 않게.
// 멈추는 건 animation-play-state 하나뿐이라 위치를 JS 로 붙잡을 필요가 없다
.tt__tape:hover .tt__track,
.tt:focus-within .tt__track { animation-play-state: paused; }

03새 값이 들어오면 왼쪽으로 밀리는 스파크라인

무엇이 보이나

줄마다 붙은 작은 추이선이 새 값 하나마다 한 칸씩 왼쪽으로 미끄러지고, 오른쪽 끝에서 새 점이 들어옵니다. 선을 통째로 다시 그리는 게 아니라 점 하나를 뒤에 붙이고 트랙을 한 칸 옮길 뿐이라, 왼쪽으로 밀려난 옛 점은 viewBox 경계가 알아서 잘라 냅니다.

새 값이 들어오면 왼쪽으로 밀리는 스파크라인
새 창에서 열기

03 · 새 값이 들어오면 왼쪽으로 밀리는 스파크라인 — 측정 누적면적 10.465% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

polylinetranslateXpreserveAspectRatio

코드에서 볼 곳

600ms 간격 갱신이 움직인 프레임 17/23 · 누적면적 10.465% 로 잡혔고, 표의 행마다 붙는 미니 추이나 서버 모니터링 화면의 초당 요청 수에 씁니다.

// 새 값 하나 = 점 하나 + 한 칸. 60개를 다시 그리지 않는 게 이 부품의 전부다
function tick(r) {
  var mid = (r.lo + r.hi) / 2, span = r.hi - r.lo;
  // 평균으로 조금씩 돌아오는 임의보행 — 눈금 밖으로 영영 달아나지 않는다
  var p = Math.round(r.price + (rand() - 0.5) * span * 0.22 - (r.price - mid) * 0.05);
  r.price = p;
  r.line.setAttribute('points', r.line.getAttribute('points') + ' ' + r.count + ',' + toY(r, p).toFixed(2));
  r.count += 1;
  r.shift += 1;
  r.track.style.transform = 'translateX(-' + r.shift + 'px)';
  var chg = (p - r.base) / r.base * 100;
  r.num.textContent = money(p);
  // 방향은 색만으로 전하지 않는다 — 화살표와 부호를 늘 같이 적는다
  r.chg.textContent = arrow(chg) + ' ' + pct(chg);
  r.chg.className = 'sp__chg ' + (chg >= 0 ? 'is-up' : 'is-down');
  if (r.count > 400) { rebase(r); }
}

04마지막 봉이 자라는 캔들 차트

무엇이 보이나

왼쪽 봉 열한 개는 값이 굳어 가만히 서 있고, 맨 오른쪽 한 봉만 260ms 마다 고가·저가를 따라 늘어나며, 열 번을 채우면 그 봉이 확정되고 차트가 한 칸 왼쪽으로 밀립니다. 오르는 봉은 속이 빈 몸통, 내리는 봉은 채운 몸통이라 색을 못 읽어도 방향이 보입니다.

마지막 봉이 자라는 캔들 차트
새 창에서 열기

04 · 마지막 봉이 자라는 캔들 차트 — 측정 누적면적 2.771% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scaleYtransform-originsteps(1, end)

기준점을 잘못 잡으면 차트가 거짓말을 한다

움직인 프레임 23/23 · 누적면적 2.771% 로 쉬는 장이 없는 부품이고, 차트 화면의 분봉 영역이나 거래 앱의 기간 전환 탭 아래에 씁니다.

.cl__wick { position: absolute; left: calc(50% - 1px); width: 2px; background: $tick-mute; }
// 심지도 몸통도 **여는 값 쪽 변**이 기준점이다. 가운데를 기준으로 늘리면 이미 확정된 여는 값 자리까지
// 같이 움직여 차트가 거짓말을 한다 — 위로 자라는 것은 아래 변, 아래로 자라는 것은 위 변이 origin
.cl__wick--up { transform-origin: 50% 100%; }
.cl__wick--dn { transform-origin: 50% 0; }
.cl__slot--live .cl__wick, .cl__slot--next .cl__wick { background: $color; }

.cl__body { box-sizing: border-box; position: absolute; left: 18%; right: 18%; border-radius: $r-xs; }
// 방향을 색만으로 전하지 않는다 — 오르는 봉은 속 빈 몸통, 내리는 봉은 채운 몸통으로 형태가 갈린다
.cl__body.is-up { border: 2px solid $color; background: transparent; transform-origin: 50% 100%; }
.cl__body.is-down { border: 2px solid $tick-down; background: $tick-down; transform-origin: 50% 0; }

05매수·매도 깊이가 늘었다 줄었다 하는 호가창

무엇이 보이나

가운데 체결가를 두고 위는 파는 쪽, 아래는 사는 쪽으로 줄이 서고, 줄마다 그 줄까지 쌓인 양이 400ms 마다 막대 길이로 다시 그려집니다. 파는 쪽 막대는 오른쪽 끝에서, 사는 쪽 막대는 왼쪽 끝에서 자라고 끝 모서리도 서로 반대로 둥글려 두었습니다.

매수·매도 깊이가 늘었다 줄었다 하는 호가창
새 창에서 열기

05 · 매수·매도 깊이가 늘었다 줄었다 하는 호가창 — 측정 누적면적 5.564% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scaleXtransform-origintransition

왜 width 가 아니라 scaleX 인가

움직인 프레임 18/23 · 누적면적 5.564% 를 기록했고, 거래 화면 오른쪽 호가창이나 유동성을 눈으로 훑는 내부 도구에 씁니다.

function repaint() {
  var rows = roll(askCells).concat(roll(bidCells));
  var max = 0;
  rows.forEach(function (r) { if (r.sum > max) { max = r.sum; } });
  rows.forEach(function (r) {
    r.el.querySelector('[data-qty]').textContent = money(r.q);
    r.el.querySelector('[data-cum]').textContent = money(r.sum);
    // 막대는 width 가 아니라 scaleX 로 늘린다 — 레이아웃을 다시 계산하지 않으니 줄이 흔들리지 않고,
    // 길이 변화는 transition 이 이어 준다
    r.el.querySelector('[data-bar]').style.transform = 'scaleX(' + (0.16 + 0.84 * r.sum / max).toFixed(3) + ')';
  });
}

06등락률이 색으로 바뀌는 격자 히트맵

무엇이 보이나

칸 크기가 모두 같은 4×3 격자에서 850ms 마다 타일 세 개의 등락률이 갈리고, 그때마다 배경색이 컷으로 튀지 않고 흐르듯 따라옵니다. 칸 크기는 아무 값도 나르지 않으니 읽는 사람이 넓이와 비중을 헷갈릴 일이 없습니다.

등락률이 색으로 바뀌는 격자 히트맵
새 창에서 열기

06 · 등락률이 색으로 바뀌는 격자 히트맵 — 측정 누적면적 29.14% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

@propertycolor-mixgrid-template-columns

색이 튀지 않고 이어지는 이유

아홉 개 중 가장 넓게 움직여 누적면적 29.14% · 움직인 프레임 16/23 이 나왔고, 보유 종목 전부를 한눈에 보는 화면이나 섹터 등락 요약 위젯에 씁니다.

.hm__tile {
  // 타일의 등락률: 미리보기는 --p 로 지금 값(--va)과 다음 값(--vb) 사이를 오가고,
  // 실사용에서는 JS 가 이 자리에 인라인으로 --chg 를 써 넣어 이 규칙을 덮는다
  --chg: calc(var(--va) + (var(--vb) - var(--va)) * var(--p));
  // 0 부근은 판 색 그대로. 위로 갈수록 상승색, 아래로 갈수록 하락색을 섞는다 (1% 당 11%, 상한 50%)
  --up-mix: clamp(0%, calc(var(--chg) * 11%), #{$mix-cap});
  --down-mix: clamp(0%, calc(var(--chg) * -11%), #{$mix-cap});
  background-color: color-mix(in oklab, $tick-down var(--down-mix),
                    color-mix(in oklab, $tick-up var(--up-mix), $tick-panel));
  // 실사용에서 값이 바뀔 때 색이 컷으로 튀지 않고 이어진다 — 등록한 숫자 속성이라 보간된다
  transition: --chg $dur-base $easing;
  display: grid; align-content: center; justify-items: center; gap: 0;
  min-height: 32px; padding: $sp-1 0; border-radius: $r-xs;
}

07자릿수가 굴러가는 큰 숫자판

무엇이 보이나

평가손익 한 덩어리가 700ms 마다 다시 굴러가는데, 자릿수마다 0부터 9까지 세로로 쌓인 띠가 올라오고 칸이 넘치는 부분을 잘라 냅니다. 천 단위 구분자와 소수점은 굴리지 않고 제자리에 박혀 있으며, 값이 0을 지나 음수가 되면 부호 칸이 함께 갈립니다.

자릿수가 굴러가는 큰 숫자판
새 창에서 열기

07 · 자릿수가 굴러가는 큰 숫자판 — 측정 누적면적 3.781% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateYtabular-numsoverflow

코드에서 볼 곳

한 번에 한두 칸만 굴러 움직인 프레임 12/23 · 누적면적 3.781% 에 그쳤고, 대시보드 맨 위 총자산 카드나 누적 거래대금 표시에 씁니다.

// 자릿수 칸 — 0 부터 9 까지 세로로 쌓은 띠를 끌어올린다. 칸은 넘치는 부분을 자른다
.od__digit { display: block; overflow: hidden; width: .64em; height: $cell; }
.od__strip {
  display: block;
  transform: translateY(calc(var(--d) * -#{$cell}));
  // 실사용에서 값이 바뀌면 여기가 굴러간다. 등록한 숫자 속성이라 9 에서 0 으로도 이어서 흐른다
  transition: --d $dur-enter $easing;
}
.od__n { display: block; height: $cell; line-height: 1.15; text-align: center; }
// 구분자와 소수점은 굴리지 않는다 — 제자리에 박혀 있어야 자릿수가 어디서 끊기는지 눈이 안 잃는다
.od__fixed { display: block; width: .3em; height: $cell; line-height: 1.15; }

08설정한 선을 넘으면 알리는 임계 배지

무엇이 보이나

슬라이더와 버튼과 키보드로 움직이는 임계선이 트랙 위에 서고, 현재가 점이 그 선을 넘는 순간에만 배지가 뜨고 소리 없는 고리가 한 번 퍼집니다. 같은 쪽에 머무는 동안에는 값이 아무리 흔들려도 다시 울리지 않고, 화면에는 「넘음」과 「아래」가 글자로도 적힙니다.

설정한 선을 넘으면 알리는 임계 배지
새 창에서 열기

08 · 설정한 선을 넘으면 알리는 임계 배지 — 측정 누적면적 2.406% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

aria-livebox-shadowsteps(1, end)

언제 울려야 한 번만 울리나

움직인 프레임 21/23 · 누적면적 2.406% 로 작게 움직이는 부품이고, 가격 알림 설정 화면이나 재고·온도처럼 선 하나를 감시하는 대시보드에 씁니다.

function paint() {
  var th = Number(rangeEl.value);
  var chg = (price - BASE) / BASE * 100;
  priceEl.textContent = money(price);
  dirEl.textContent = arrow(chg) + ' ' + pct(chg);
  dirEl.className = 'at__dir at__dir--next ' + (chg >= 0 ? 'is-up' : 'is-down');
  thEl.textContent = money(th);
  alertValEl.textContent = money(th);
  // 홀더는 트랙 폭과 같아서 translateX 의 % 가 곧 트랙 위 위치다 (점 자신의 폭이 기준이 되지 않는다)
  dotEl.style.transform = 'translateX(' + posOf(price).toFixed(1) + '%)';
  lineEl.style.transform = 'translateX(' + posOf(th).toFixed(1) + '%)';
  // 임계 통과는 "이전 상태와 지금 상태가 선을 사이에 두고 갈릴 때" 만 한 번 발동한다.
  // 같은 쪽에 머무는 동안에는 값이 아무리 움직여도 다시 울리지 않는다
  var over = price >= th;
  if (wasOver === false && over) { fire(); }
  wasOver = over;
  root.classList.toggle('is-over', over);
}

09연결 상태와 갱신 지연 표시

무엇이 보이나

시세판 머리에 라이브 점이 1초에 한 번 숨을 쉬고 그 옆에 마지막 갱신이 몇 초 전인지를 숫자로 셉니다. 값이 한동안 안 들어오면 상태가 「실시간」에서 「연결 끊김」으로, 이어 「다시 연결하는 중」으로 스스로 넘어가고, 점 색도 같이 갈립니다.

연결 상태와 갱신 지연 표시
새 창에서 열기

09 · 연결 상태와 갱신 지연 표시 — 측정 누적면적 0.926% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

visibilityStateanimation-delayopacity

탭을 가리면 왜 세는 것을 멈추나

누적면적 0.926% 로 아홉 개 중 가장 조용하지만 움직인 프레임은 23/23 이고, 실시간 화면 오른쪽 위 상태 표시나 재연결 안내 띠에 씁니다.

function step() {
  if (root.classList.contains('is-demo')) { return; }
  // 탭이 안 보이면 세지 않는다 — 안 보이는 화면의 지연 숫자는 뜻이 없고, 돌아왔을 때
  // 밀린 갱신이 한꺼번에 터지지도 않게 한다 (document.visibilityState)
  if (document.visibilityState === 'hidden') { return; }
  var since = Date.now() - lastAt;
  if (phase === LIVE) {
    // 다섯 번 받고 나면 값이 안 오는 상황을 흉내 낸다 — 지연이 쌓이면 상태가 스스로 넘어간다
    if (ticks > 0 && ticks % 5 === 0) {
      if (since > 2600) { phase = LOST; setState(LOST); }
    } else if (since > 1400) { feed(); }
  } else if (phase === LOST) {
    if (since > 4200) { phase = RETRY; setState(RETRY); }
  } else if (since > 6000) {
    phase = LIVE; setState(LIVE); feed();
  }
  showLag();
}

어디서 깨지는가 — 함정

이 편에서 가장 비싸게 배운 것은 overflow: hidden 은 잘라 보여 줄 뿐 요소의 max-content 를 줄이지 않는다는 사실입니다. 02 테이프 상자에 width: 100% 를 주었더니 480px 무대가 1,047px 로 벌어져 가로 스크롤바가 떴습니다 — 트랙은 두 벌을 이어 붙인 max-content 폭을 갖는데 부모 폭이 불확정이면 그 폭을 그대로 요구하기 때문입니다. 폭을 min(340px, 84vw) 로 못박고서야 무대가 제 크기로 돌아왔습니다. 04 는 다른 쪽에서 넘쳤습니다: 툴팁 left 를 px 로 계산해 박았는데 렌더러가 리로드 없이 뷰포트만 480에서 320으로 줄이니 px 값이 낡은 채 남아 판 밖으로 튀어나갔고, resize 에서 다시 계산해 max-width: 100% 가드를 걸어 잡았습니다. 색 쪽 함정은 두 번이었습니다 — 하락색으로 쓰던 $error (#c2361a) 가 시세판 표면 #231f28 위에서 2.96:1 밖에 안 나와 본문 기준 4.5:1 에 한참 못 미쳤고 #ff7a5e 로 바꿔 같은 표면 위 6.31:1 을 얻었으며, 06 타일 혼합률을 55% 까지 올렸을 때는 밝아진 타일 위 크림 글자가 4.35:1 로 떨어져 상한을 50% 로 내렸습니다(상승 50% 타일 #3c775e 위 4.93:1 · 하락 50% 타일 #8a4c45 위 6.16:1). 마지막은 눈의 문제였습니다: 07 에서 여섯 자리를 동시에 굴렸더니 대표 이미지가 통째로 흐려져, 오른쪽 끝 자리부터 3% 씩 밀리는 실제 자리올림 순서로 바꿔 한 프레임에 한두 칸만 움직이게 했습니다.

무엇이 깨졌나 실측 어떻게 고쳤나
02 테이프 상자 width: 100% 480px 무대가 1,047px 로 벌어짐 폭을 min(340px, 84vw) 로 고정
04 툴팁 left 를 px 로 고정 뷰포트만 320px 로 줄자 판 밖으로 나감 resize 에서 재계산 + max-width: 100%
하락색 $error (#c2361a) #231f28 위 2.96:1 로 AA 미달 #ff7a5e — 같은 표면 위 6.31:1
06 타일 혼합률 55% 크림 글자 4.35:1 로 미달 상한 50%, 글자색은 크림 하나로
07 여섯 자리 동시 굴림 대표 이미지가 통째로 흐려짐 오른쪽 끝부터 3% 씩 밀리는 자리올림

한 화면에서 여러 줄이 동시에 갱신될 때 어디를 봐야 할지 잃는 문제는 알림 목록 ui의 활동 피드에서도 같은 모양으로 나타납니다.

아홉 개를 한 묶음으로 받는 압축 파일의 암호는 m5tz6sq7 이고, 열면 항목마다 브라우저에서 바로 열리는 HTML 한 벌과 props 로 시드·간격을 받는 tsx 한 벌이 나란히 들어 있습니다.

접근성

주식 ui 에서 오름과 내림을 색으로만 말하면 색각 이상이 있는 사람에게는 그냥 숫자만 바뀌는 화면이 됩니다. 그래서 아홉 개 전부 숫자 옆에 ▲/▼ 와 +/− 부호를 같이 적었고, 04 는 몸통을 비우거나 채워 형태로도 방향을 갈랐으며, 08 은 「넘음 / 아래」를, 09 는 점 옆에 상태 이름을 글자로 둡니다. aria-live="polite" 는 08 의 알림 한 줄에만 붙였습니다 — 갱신되는 가격 칸에 붙이면 화면 낭독기가 시세를 끝없이 읽어 대서 화면 자체를 쓸 수 없게 됩니다(MDN aria-live). 깜빡임은 2초 루프 기준으로 01 이 세 번(초당 1.5회), 08 과 09 가 두 번(초당 1회)이고 02 와 03 은 아예 0회라, 초당 3회 이하를 요구하는 WCAG 2.3.1 아래에 들어갑니다.

prefers-reduced-motion: reduce 에서는 연출만 끄고 값 갱신은 남깁니다. 01 은 플래시 배경을 지우고 현재값만, 02 는 테이프를 맨 앞에 세운 채 숫자만, 03 은 밀기를 멈추고 마지막 값 한 장만 남습니다. 04 는 자라는 연출과 확정 컷을 멈춰 마지막 봉이 다 자란 크기로 서고, 05 는 막대 transition 을 끄되 길이는 그대로 갱신되며, 06 은 색이 흐르는 것과 숫자 교체만 멈춥니다. 07 은 굴림과 색 흐름을 멈춰 값이 제자리에 서고, 08 은 펄스와 점 이동을 끈 채 선을 넘은 뒤 상태를 그대로 보여 주며, 09 는 숨결과 점멸을 멈추고 상태 글자와 지연 숫자만 남깁니다. 09 의 document.visibilityState 는 탭이 보이지 않는 동안 타이머가 아예 세지 않게 해, 돌아왔을 때 밀린 갱신이 한꺼번에 터지지 않게 막습니다.

FAQ

실시간 차트를 붙이려면 서버나 웹소켓이 꼭 있어야 하나요?

화면 쪽은 없어도 됩니다. 아홉 개 모두 시드를 받는 난수 발생기가 setInterval 로 다음 값을 만들어 DOM 에 쓰는 구조라, 값을 만드는 그 함수 하나를 fetch 응답이나 웹소켓 메시지로 바꿔 끼우면 그리는 코드는 손댈 곳이 없습니다. 시드를 고정해 두었기 때문에 누가 열어도 같은 흐름이 재현되고, 디자인 리뷰나 스크린샷을 찍을 때 값이 매번 달라지지 않는 것도 덤입니다.

숫자가 바뀔 때마다 화면 낭독기가 읽게 하면 안 되나요?

안 됩니다. aria-live 를 갱신되는 가격 칸에 붙이면 초당 한두 번씩 들어오는 값이 전부 낭독 대기열에 쌓여 정작 중요한 알림이 묻힙니다. 이 편이 택한 기준은 사람이 미리 정한 조건을 만족했을 때만 소리로 말한다이고, 그래서 08 의 임계 알림 한 줄에만 aria-live="polite" 를 두고 나머지 시세는 눈으로 보는 정보로 남겼습니다. 값 자체를 소리로 확인해야 하는 화면이라면 낭독용 요약 문장을 따로 만들어 몇 초에 한 번만 갱신하는 편이 낫습니다. 조건을 만족했을 때 띄우는 알림 자체의 쌓임·사라짐은 토스트 알림 UI에서 아홉 가지로 정리했습니다.

숫자가 바뀔 때 자릿수가 흔들리는 건 어떻게 막나요?

font-variant-numeric: tabular-nums 한 줄이면 됩니다. 대부분의 본문용 글꼴은 1이 좁고 0이 넓은 비례 숫자를 쓰기 때문에 64,200 이 64,311 로만 바뀌어도 오른쪽 끝이 흔들리는데, 이 속성은 모든 숫자 글리프의 폭을 같게 맞춥니다(MDN font-variant-numeric, caniuse 기준 전역 96.75%). 폭이 달라지는 게 자릿수가 아니라 값의 개수 자체라면 01 처럼 두 장을 같은 칸에 겹쳐 컷으로 갈아 끼우고, 위젯을 카드 단위로 떼어 쓰는 방법은 대시보드 위젯 9종에 정리해 두었습니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.