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

자동저장 표시 9종 — 값이 초 단위로 갱신되는 저장 상태 아홉 가지

자동저장 표시는 지금 저장이 어느 상태에 있는지를 값 하나로 계산해 화면에 드러내는 인터랙션을 말하는데, 아홉 개 중 하나는 저장 중이라는 문구가 점 세 개와 함께 반복되고, 하나는 마지막 저장 시각 값이 초 단위로 갱신되며 몇 분 전이라는 말로 바뀌고, 또 하나는 오프라인일 때 대기

아홉 항목 전부 저장 관련 버튼(또는 09는 카드 전체) click 이벤트에 반응해 값을 재설정하는 결정론적 계산이며 난수는 0 — 같은 순서로 클릭하면 항상 같은 값이 나옵니다. 이런 계산은 문서 편집기의 자동저장 기능이나 폼의 자동저장 위치 표시줄에 흔히 쓰이는 저장 상태 계산과 같은 뼈대이지만, 화면 속 저장·전송·오프라인은 전부 가상 시뮬레이션이라 실제 fetch·navigator.onLine·localStorage 호출은 하나도 없고 실서버·실계정 정보도 담지 않습니다.

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

아홉 개의 해부 — 어떤 값이 언제 바뀌는가

아홉 항목 모두 버튼(01 지금 저장·02 변경 저장·03 다시 저장·04 연결 전환·05 지금 저장·06 다음 단계·07 내 변경 유지·새로고침 중 하나·08 지금 저장·09 카드 전체) click 이벤트로 값을 재설정·전환할 수 있고, 로드 즉시 도는 자동 시연 루프는 값을 미리 보여주는 보조 장치일 뿐 리스너 자체를 대신하지 않습니다.

번호 이름 리스너 누적면적% 강도 움직인 프레임/23
01 저장 중 문구 펄스 click(지금 저장 버튼) 0.41 39.1 19
02 저장 완료 값 반짝 click(변경 저장 버튼) 0.64 53.4 11
03 저장 실패 재시도 카운트 click(다시 저장 버튼) 1.65 109.5 10
04 오프라인 대기 건수 click(연결 전환 버튼) 1.76 148.7 22
05 마지막 저장 시각 갱신 click(지금 저장 버튼) 0.82 104.4 21
06 상태 점 색 순환 click(다음 단계 버튼) 0.89 60.4 18
07 저장 충돌 안내 click(선택 버튼 — 내 변경 유지·새로고침) 11.11 127.8 10
08 다음 자동저장 카운트다운 click(지금 저장 버튼) 2.63 158.9 18
09 모션 감소 정적 저장 표시 click(카드 — 두 패널 값 동시 갱신) 2.27 57.7 16

09는 컴포넌트 편 성격이 강해 면적이 작은 것이 정상입니다 — 면적이 아니라 스프링·물성 한 겹 같은 정교함으로 재는 항목입니다. 아홉 항목이 쓰는 계산식을 겹치는 것끼리 묶으면 아래 다섯 무리로 정리됩니다.

감쇠 사인으로 밝기가 오르내리는 값 — 01·09

01과 09는 같은 계산을 공유합니다 — 0.5 + 0.5×sin(t·2π) 로 만든 사인 파형이 저장 중 문구·배지의 밝기(opacity) 값을 PULSE_PERIOD_MS 주기로 오르내리게 합니다. 01은 이 파형을 점 세 개에 DOT_STAGGER_MS 만큼 위상을 어긋내 순서대로 밝히고, 09는 같은 파형을 쓰는 일반 모션 패널(A)과 파형 없이 클래스만 즉시 바뀌는 패널(B)을 나란히 놓아 두 계산의 차이를 눈으로 대조합니다.

시간 비율로 그려지는 값 — 02·08

02는 DRAW_MS 동안 흐른 시간의 비율(t)을 SVG 체크 경로의 stroke-dashoffset 값(길이×(1−t))으로 바꿔 체크 표시를 그리고, 08은 반대로 남은 초의 비율을 원형 링의 stroke-dashoffset 값(둘레×(1−remaining/CYCLE_SEC))으로 바꿔 줄어드는 링을 그립니다. 둘 다 getTotalLength·2πR 로 구한 실제 길이에 비율 하나만 곱해 그리기 진행을 표현합니다.

기하급수·문턱으로 늘거나 줄어드는 값 — 03·04

03은 실패 순번을 지수의 자리에 넣어 BACKOFF_BASE_MS × 2^순번 으로 다음 시도까지 대기 값을 두 배씩 불리고, 04는 실패 대신 오프라인 지속시간을 셈해 대기 건수 값이 ADD_MS 간격으로 하나씩 늘다 MAX_QUEUE 문턱에 닿는 순간 온라인으로 뒤집혀 값이 거꾸로 줄어듭니다. 하나는 대기 시간이 지수로 커지고 하나는 건수가 문턱에서 방향을 바꾼다는 점이 다릅니다.

스스로 진행하는 타이머 값 — 06·07

06은 STEP_MS 마다 stageIndex 값이 모듈로 연산으로 다음 단계를 돌고, 07은 APPEAR_MS 마다 스스로 나타나는 배너 위치 값을 스프링-감쇠 물리(−SPRING_K×변위 − SPRING_C×속도)로 목표까지 되돌립니다. 두 계산 모두 클릭이 없어도 자체 타이머 체인으로 계속 돌지만, 06은 이산적인 모듈로 점프이고 07은 연속적인 물리 적분이라는 점이 다릅니다.

경과 시간이 스스로 늘어나는 값 — 05

05만은 다섯 갈래 중 유일하게 클릭 시점을 기준으로 경과 시간 값이 프레임마다 스스로 계속 자라나며((지금−저장시각)÷DEMO_SEC_MS), 그 값이 MIN_THRESHOLD_SEC·RESET_AFTER_SEC 두 문턱을 지날 때마다 문구와 밝기가 함께 바뀝니다.

패턴별 코드

01저장 중 문구 펄스 — 저장이 진행되는 동안 문구의 밝기 값이 오르내리기를 반복하는 방식

지금 저장 버튼을 클릭하면 SAVING_MS 2400ms 동안 저장 중 문구가 유지되고, 그동안 문구와 점 세 개의 밝기 값이 PULSE_PERIOD_MS 900ms 주기의 사인 파형으로 오르내립니다. 점 세 개는 DOT_STAGGER_MS 160ms 씩 서로 위상을 어긋내 순서대로 밝아지고, SAVING_MS 가 지나면 저장됨으로 바뀌었다가 IDLE_MS 1000ms 뒤 다시 저장 중으로 돌아갑니다.

      if (reduce) {
        label.style.opacity = '1';
        dots.forEach(function (d) { d.style.opacity = '.6'; });
      } else {
        var t = (el % PULSE_PERIOD_MS) / PULSE_PERIOD_MS;
        var op = 0.45 + 0.55 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2));
        label.style.opacity = op.toFixed(3);
        dots.forEach(function (d, i) {
          var dop = 0.2 + 0.8 * Math.abs(Math.sin((el - i * DOT_STAGGER_MS) / PULSE_PERIOD_MS * Math.PI));
          d.style.opacity = dop.toFixed(3);
        });
      }
저장 중 문구 펄스 — 저장이 진행되는 동안 문구의 밝기 값이 오르내리기를 반복하는 방식
새 창에서 열기

01 · 저장 중 문구 펄스 — 저장이 진행되는 동안 문구의 밝기 값이 오르내리기를 반복하는 방식 — 측정 누적면적 0.408% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PULSE_PERIOD_MSDOT_STAGGER_MSSAVING_MS

02저장 완료 값 반짝 — 저장이 끝나는 순간 완료 값이 1로 바뀌었다가 일정 시간 뒤 다시 0으로 사라지는 방식

변경 저장 버튼을 클릭하면 draw() 가 SVG 체크 경로의 실제 길이(getTotalLength)를 기준으로 DRAW_MS 300ms 동안 stroke-dashoffset 값을 길이×(1−t) 로 줄여 체크 표시를 그립니다. 다 그려지면 HOLD_MS 380ms 유지된 뒤 FADE_MS 200ms 에 걸쳐 사라지고, GAP_MS 260ms 뒤 같은 그리기가 반복됩니다.

  function draw(cb) {
    if (reduce) { check.style.strokeDashoffset = '0'; cb(); return; }
    var start = null;
    function frame(ts) {
      if (start === null) start = ts;
      var t = Math.min(1, (ts - start) / DRAW_MS);
      check.style.strokeDashoffset = (len * (1 - t)).toFixed(2);
      if (t < 1) requestAnimationFrame(frame); else cb();
    }
    requestAnimationFrame(frame);
  }
저장 완료 값 반짝 — 저장이 끝나는 순간 완료 값이 1로 바뀌었다가 일정 시간 뒤 다시 0으로 사라지는 방식
새 창에서 열기

02 · 저장 완료 값 반짝 — 저장이 끝나는 순간 완료 값이 1로 바뀌었다가 일정 시간 뒤 다시 0으로 사라지는 방식 — 측정 누적면적 0.64% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

DRAW_MSHOLD_MSGAP_MS

03저장 실패 재시도 카운트 — 저장이 실패하면 남은 재시도 횟수 값이 하나씩 줄며 문구가 흔들리는 방식

다시 저장 버튼을 클릭할 때마다 fail() 이 실행되어 남은 재시도 값(MAX_RETRY 3에서 시작)이 하나씩 줄고, 그 순간 shake() 가 감쇠 사인 곡선(AMP 5 × sin(t·2π·3) × (1−t))으로 SHAKE_MS 260ms 동안 카드를 짧게 흔듭니다. 다음 실패까지 대기 시간은 BACKOFF_BASE_MS 260 × 2^순번 으로 두 배씩 늘어나다 재시도가 0이 되면 재시도 소진 문구로 바뀝니다.

  function shake(cb) {
    if (reduce) { cb(); return; }
    var start = null;
    function frame(ts) {
      if (start === null) start = ts;
      var t = Math.min(1, (ts - start) / SHAKE_MS);
      var x = AMP * Math.sin(t * Math.PI * 2 * 3) * (1 - t);
      card.style.transform = 'translateX(' + x.toFixed(2) + 'px)';
      if (t < 1) requestAnimationFrame(frame); else { card.style.transform = 'translateX(0)'; cb(); }
    }
    requestAnimationFrame(frame);
  }
저장 실패 재시도 카운트 — 저장이 실패하면 남은 재시도 횟수 값이 하나씩 줄며 문구가 흔들리는 방식
새 창에서 열기

03 · 저장 실패 재시도 카운트 — 저장이 실패하면 남은 재시도 횟수 값이 하나씩 줄며 문구가 흔들리는 방식 — 측정 누적면적 1.646% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MAX_RETRYSHAKE_MSBACKOFF_BASE_MS

04오프라인 대기 건수 — 연결이 끊긴 동안 쌓인 대기 저장 건수 값이 연결이 돌아오는 순간 0으로 줄어드는 방식

연결 전환 버튼을 클릭해 오프라인으로 바꾸면 ADD_MS 260ms 마다 대기 건수 값이 하나씩 늘어 막대 채움이 count ÷ MAX_QUEUE 비율로 커지고, MAX_QUEUE 6에 닿으면 스스로 온라인으로 바뀌어 DRAIN_MS 150ms 마다 그 값이 다시 0까지 줄어듭니다. 연결·해제는 실제 navigator.onLine 이 아니라 온라인 여부를 담은 불리언 변수 하나로만 흉내 낸 값입니다.

  function tick() {
    if (!online) {
      count = Math.min(MAX_QUEUE, count + 1);
      paint();
      if (count >= MAX_QUEUE) { online = true; paint(); timer = setTimeout(tick, DRAIN_MS); return; }
      timer = setTimeout(tick, ADD_MS);
    } else {
      count = Math.max(0, count - 1);
      paint();
      if (count <= 0) { timer = setTimeout(function () { online = false; paint(); timer = setTimeout(tick, ADD_MS); }, 500); return; }
      timer = setTimeout(tick, DRAIN_MS);
    }
  }
오프라인 대기 건수 — 연결이 끊긴 동안 쌓인 대기 저장 건수 값이 연결이 돌아오는 순간 0으로 줄어드는 방식
새 창에서 열기

04 · 오프라인 대기 건수 — 연결이 끊긴 동안 쌓인 대기 저장 건수 값이 연결이 돌아오는 순간 0으로 줄어드는 방식 — 측정 누적면적 1.761% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MAX_QUEUEADD_MSDRAIN_MS

05마지막 저장 시각 갱신 — 마지막으로 저장된 뒤 지난 시간 값이 초마다 갱신되며 문구로 바뀌는 방식

지금 저장 버튼을 클릭한 시각을 기준으로 raf() 가 매 프레임 (지금−저장시각)÷DEMO_SEC_MS 90 으로 경과 초를 다시 계산해, MIN_THRESHOLD_SEC 60초 미만이면 몇 초 전, 넘으면 몇 분 전 문구로 값을 바꿉니다. 점의 밝기는 1 − min(1, 경과÷RESET_AFTER_SEC 150) 신선도 값으로 서서히 낮아지다가 150초에 닿으면 저장 시각이 다시 지금으로 리셋됩니다.

  function raf(ts) {
    var elapsedSec = Math.floor((ts - savedAt) / DEMO_SEC_MS);
    if (elapsedSec >= RESET_AFTER_SEC) { savedAt = ts; elapsedSec = 0; }
    var phrase;
    if (elapsedSec < 5) phrase = '방금 저장됨';
    else if (elapsedSec < MIN_THRESHOLD_SEC) phrase = elapsedSec + '초 전 저장됨';
    else phrase = Math.floor(elapsedSec / 60) + '분 전 저장됨';
    label.textContent = phrase;
    var fresh = 1 - Math.min(1, elapsedSec / RESET_AFTER_SEC);
    dot.style.opacity = reduce ? '1' : (0.35 + 0.65 * fresh).toFixed(3);
    requestAnimationFrame(raf);
  }
마지막 저장 시각 갱신 — 마지막으로 저장된 뒤 지난 시간 값이 초마다 갱신되며 문구로 바뀌는 방식
새 창에서 열기

05 · 마지막 저장 시각 갱신 — 마지막으로 저장된 뒤 지난 시간 값이 초마다 갱신되며 문구로 바뀌는 방식 — 측정 누적면적 0.822% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

DEMO_SEC_MSMIN_THRESHOLD_SECRESET_AFTER_SEC

06상태 점 색 순환 — 대기·진행·완료 세 단계에 따라 점의 색상 값이 차례로 바뀌는 방식

다음 단계 버튼을 클릭하거나 STEP_MS 480ms 마다 stageIndex 값이 STAGE_COUNT 3으로 나눈 나머지로 넘어가고, 점 i번째의 표시 단계는 (stageIndex − i + 3) % 3 모듈로 계산으로 정해져 점마다 한 박자씩 늦게 따라갑니다. 계산된 단계 값은 CLASSES 배열의 대기·진행·완료 클래스 이름으로 바로 매핑됩니다.

  function paint() {
    label.textContent = NAMES[stageIndex];
    dots.forEach(function (d, i) {
      var s = ((stageIndex - i) % STAGE_COUNT + STAGE_COUNT) % STAGE_COUNT;
      d.className = 'sdc__dot ' + CLASSES[s];
    });
  }
  paint();

  function step() {
    stageIndex = (stageIndex + 1) % STAGE_COUNT;
    paint();
    timer = setTimeout(step, STEP_MS);
  }
  timer = setTimeout(step, STEP_MS);
상태 점 색 순환 — 대기·진행·완료 세 단계에 따라 점의 색상 값이 차례로 바뀌는 방식
새 창에서 열기

06 · 상태 점 색 순환 — 대기·진행·완료 세 단계에 따라 점의 색상 값이 차례로 바뀌는 방식 — 측정 누적면적 0.888% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STAGE_COUNTSTEP_MSCLASSES

07저장 충돌 안내 — 같은 항목이 다른 곳에서 먼저 저장되면 안내 값이 나타나 선택을 기다리는 방식

APPEAR_MS 1400ms 마다 스스로 저장 충돌 배너가 나타나고, 배너 위치 값은 속도 += (−SPRING_K 200×변위 − SPRING_C 19×속도)×dt 스프링-감쇠 물리로 목표 위치까지 적분됩니다. 내 변경 유지·새로고침 두 버튼 중 하나를 클릭하지 않으면 AUTO_RESOLVE_MS 850ms 뒤 스스로 닫히고 다음 충돌을 준비합니다.

  function tick(ts, last) {
    var dt = Math.min(32, ts - last) / 1000;
    var disp = pos - target;
    var acc = -SPRING_K * disp - SPRING_C * vel;
    vel += acc * dt;
    pos += vel * dt;
    banner.style.transform = 'translateY(' + pos.toFixed(2) + '%)';
    if (Math.abs(disp) > 0.3 || Math.abs(vel) > 0.5) {
      rafId = requestAnimationFrame(function (t) { tick(t, ts); });
    } else {
      pos = target;
      banner.style.transform = 'translateY(' + pos.toFixed(2) + '%)';
    }
  }

  function spring(to) {
    target = to;
    if (reduce) { pos = to; vel = 0; banner.style.transform = 'translateY(' + pos + '%)'; return; }
    if (rafId) cancelAnimationFrame(rafId);
    rafId = requestAnimationFrame(function (ts) { tick(ts, ts - 16); });
  }
저장 충돌 안내 — 같은 항목이 다른 곳에서 먼저 저장되면 안내 값이 나타나 선택을 기다리는 방식
새 창에서 열기

07 · 저장 충돌 안내 — 같은 항목이 다른 곳에서 먼저 저장되면 안내 값이 나타나 선택을 기다리는 방식 — 측정 누적면적 11.108% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPRING_KSPRING_CAUTO_RESOLVE_MS

08다음 자동저장 카운트다운 — 다음 저장까지 남은 시간 값이 0을 향해 줄어드는 링 방식

지금 저장 버튼을 클릭하기 전까지 remaining 값이 CYCLE_SEC 10초에서 DEMO_TICK_MS 130ms 간격으로 하나씩 줄고, 링의 stroke-dashoffset 값은 둘레(2πR, R 40) × (1 − remaining÷CYCLE_SEC) 비율로 함께 줄어듭니다. 0에 닿으면 저장 중 상태로 바뀐 뒤 500ms 뒤 CYCLE_SEC 값으로 다시 시작합니다.

  function paint() {
    numEl.textContent = String(remaining);
    var offset = CIRC * (1 - remaining / CYCLE_SEC);
    prog.style.strokeDashoffset = offset.toFixed(2);
    root.classList.toggle('sac--saving', remaining <= 0);
  }

  function start() {
    remaining = CYCLE_SEC;
    root.classList.remove('sac--saving');
    paint();
    if (timerId) clearInterval(timerId);
    timerId = setInterval(function () {
      remaining -= 1;
      if (remaining <= 0) {
        remaining = 0;
        paint();
        clearInterval(timerId);
        timerId = setTimeout(start, 500);
        return;
      }
      paint();
    }, DEMO_TICK_MS);
  }
  start();
다음 자동저장 카운트다운 — 다음 저장까지 남은 시간 값이 0을 향해 줄어드는 링 방식
새 창에서 열기

08 · 다음 자동저장 카운트다운 — 다음 저장까지 남은 시간 값이 0을 향해 줄어드는 링 방식 — 측정 누적면적 2.631% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CYCLE_SECRDEMO_TICK_MS

09모션 감소 정적 저장 표시 — 모션 감소 설정에서 애니메이션 대신 문구 값만 그대로 바뀌는 방식

카드를 클릭하면 두 패널의 문구 값이 동시에 저장 중·저장됨으로 바뀌지만, 일반 모션 패널(A)의 배지만 raf() 의 사인 파형으로 SAVING_MS 900ms 동안 밝기 값이 오르내리고 모션 감소 패널(B)의 배지는 opacity 계산 없이 클래스만 즉시 바뀝니다. 방문자의 prefers-reduced-motion 이 reduce 이면 두 패널 모두 requestAnimationFrame 대신 SAVING_MS 간격의 setInterval 로 상태 값을 즉시 스냅해 전환합니다.

  function raf(ts) {
    if (phaseStart === null) phaseStart = ts;
    var el = ts - phaseStart;
    if (phase === 'saving') {
      var t = (el % PULSE_PERIOD_MS) / PULSE_PERIOD_MS;
      badgeA.style.opacity = (0.5 + 0.5 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2))).toFixed(3);
      if (el > SAVING_MS) {
        phase = 'idle'; phaseStart = ts;
        labelA.textContent = labelB.textContent = '저장됨';
        badgeA.style.opacity = '1';
        badgeA.classList.add('srm__badge--done'); badgeB.classList.add('srm__badge--done');
      }
    } else {
      if (el > IDLE_MS) {
        phase = 'saving'; phaseStart = ts;
        labelA.textContent = labelB.textContent = '저장 중';
        badgeA.classList.remove('srm__badge--done'); badgeB.classList.remove('srm__badge--done');
      }
    }
    requestAnimationFrame(raf);
  }
모션 감소 정적 저장 표시 — 모션 감소 설정에서 애니메이션 대신 문구 값만 그대로 바뀌는 방식
새 창에서 열기

09 · 모션 감소 정적 저장 표시 — 모션 감소 설정에서 애니메이션 대신 문구 값만 그대로 바뀌는 방식 — 측정 누적면적 2.274% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SAVING_MSIDLE_MSPULSE_PERIOD_MS

값이 패턴을 바꾸는 곳 — 갱신 주기·문구 전환 시점·색상 단계의 기준

갱신 주기 값 — PULSE_PERIOD_MS·DOT_STAGGER_MS·STEP_MS

01·09의 PULSE_PERIOD_MS(각각 900·500)는 밝기 사인 파형이 한 바퀴 도는 시간을 정하고, 01의 DOT_STAGGER_MS 160은 점 세 개가 서로 얼마나 어긋나 밝아지는지를, 06의 STEP_MS 480은 점 색이 다음 단계로 넘어가는 간격을 정합니다. 값이 작을수록 깜빡임·순환이 더 빠르게 느껴집니다.

문구가 바뀌는 시점 값 — SAVING_MS·IDLE_MS·MIN_THRESHOLD_SEC

01·09의 SAVING_MS(2400·900)는 저장 중 문구가 유지되는 시간이고, IDLE_MS(1000·700)는 저장됨 문구가 유지되는 시간입니다. 05의 MIN_THRESHOLD_SEC 60은 몇 초 전에서 몇 분 전으로 문구가 바뀌는 경계이고, 02의 HOLD_MS 380·GAP_MS 260은 체크 표시가 머무는 시간과 다음 반복까지의 간격입니다.

색상·단계·재시도의 기준 값 — STAGE_COUNT·MAX_RETRY·MAX_QUEUE

06의 STAGE_COUNT 3은 점 색이 대기·진행·완료 몇 단계를 도는지를 정하고, 03의 MAX_RETRY 3은 재시도 값이 0에 닿기까지 허용하는 실패 횟수를, 04의 MAX_QUEUE 6은 대기 건수 값이 온라인으로 뒤집히는 문턱을 정합니다. 07의 SPRING_K 200·SPRING_C 19는 배너가 목표 위치로 되돌아가는 세기와 감쇠를, 08의 CYCLE_SEC 10·R 40은 카운트다운 시작 값과 링의 반지름을 정합니다.

어디서 깨지는가 — 오프라인 복귀 순간·다중 탭 동시 저장·모션 감소

아홉 항목 모두 저장 중·재시도·대기 건수 같은 값을 페이지 메모리(JS 변수)에만 담아 두므로, 새로고침 한 번이면 값이 처음으로 되돌아갑니다. 04의 오프라인 전환은 실제 navigator.onLine 이벤트가 아니라 온라인 여부를 담은 불리언 변수 하나를 버튼이 뒤집는 것뿐이고, 저장·전송도 전부 가상 시뮬레이션이라 실제 fetch·XMLHttpRequest·localStorage 호출은 하나도 없습니다 — "서버에 저장된다"거나 "실제 오프라인을 감지한다"는 뜻이 아니라 데모 안에서 상태 값이 순환하는 것뿐입니다. 07의 저장 충돌 안내도 마찬가지로 다른 탭이나 다른 사용자를 실제로 감지하지 않고 APPEAR_MS 마다 스스로 충돌을 지어내는 반복일 뿐이라, 탭 두 개를 열어 동시에 눌러도 두 탭은 서로의 존재를 전혀 모른 채 각자의 변수(remaining·fails·count 등)를 따로 계산합니다. 모션 감소 쪽은 prefers-reduced-motion 이 켜지면 01·02·03·05·07 다섯 항목이 각 index.html의 if (reduce) 분기에서 사인 파형·스프링 적분을 건너뛰고 값을 즉시 최종 상태로 바꾸며, 04·06·08 세 항목은 JS 계산은 그대로 두고 style.scss의 @media (prefers-reduced-motion: reduce) 규칙으로 transition 만 꺼 CSS 전환만 사라집니다. 09는 두 방식을 나란히 보여주는 항목이라 reduce 가 켜지면 requestAnimationFrame 대신 SAVING_MS 간격의 setInterval 로 두 패널 모두 상태를 즉시 스냅해 전환합니다. 이 분기가 항목마다 다르게 갈라지는 지점을 직접 눈으로 맞춰 보려면 압축 파일을 받아 여는 편이 빠른데, 비번 3z36yr9y 로 열면 아홉 항목의 html·scss·js 원본이 그대로 들어 있습니다.

변형 세 가지와 접근성

이름 값 느낌
펄스 주기 두 배로 01 PULSE_PERIOD_MS 900 → 1800 펄스 주기를 900→1800ms로 늘리면 저장 중 문구가 두 배 느리게 숨쉬듯 밝아졌다 어두워진다
점 순환 속도 올리기 06 STEP_MS 480 → 200 단계 전환 간격을 480→200ms로 줄이면 점 세 개가 서로를 바짝 뒤쫓듯 빠르게 넘어간다
카운트다운 절반으로 08 CYCLE_SEC 10 → 5 카운트다운 초를 10→5로 줄이면 링이 두 배 빠르게 돌아 저장 예고가 더 급박해 보인다

아홉 항목의 접근성 속성은 코드를 직접 뒤진 값만 씁니다. aria-*·role·tabindex 를 붙인 곳은 01의 점 세 개 묶음에 걸린 aria-hidden="true" 하나뿐이라, 값이 바뀌는 동안 스크린 리더는 그 점들을 건너뛰고 옆의 문구 텍스트만 읽습니다. 나머지 여덟 항목은 aria 속성이 전혀 없고, 애니메이션이 도는 동안에도 버튼에 disabled 를 거는 곳이 하나도 없어 재생 중에 다시 눌러 값을 초기화할 수 있습니다.

FAQ

자동저장 표시 값은 새로고침하면 왜 초기화되나요?

아홉 항목 모두 남은 재시도·대기 건수·경과 시간 값을 서버가 아니라 페이지 메모리(JS 변수)에만 담아 두기 때문입니다. 실제 자동저장 기능이라면 이 값을 서버 세션이나 클라이언트 저장소로 남겨 새로고침에도 이어가야 하지만, 이 데모는 계산 방식만 보여주는 화면이라 값 저장소를 두지 않았습니다.

02의 체크 그리기와 08의 링 줄어들기는 같은 계산인가요?

아니요. 02는 흐른 시간의 비율을 SVG 체크 경로 길이에 곱해 길이×(1−t)로 값을 늘려가며 그리고, 08은 반대로 남은 시간의 비율을 링 둘레에 곱해 둘레×(1−remaining/CYCLE_SEC)로 값을 줄입니다. 하나는 지나간 시간(t)이 늘어날수록 그려지고, 하나는 남은 시간이 줄어들수록 사라진다는 방향이 다릅니다.

07의 스프링 값을 다른 항목에 그대로 옮겨 써도 되나요?

값 자체는 옮길 수 있지만 체감은 달라집니다. 07의 SPRING_K 200·SPRING_C 19는 배너 위치 값 하나만 되돌리도록 맞춰진 상수라, 예를 들어 저장 아이콘 디스켓 모양 배지의 크기 값처럼 변위 범위가 다른 대상에 그대로 옮기면 튀는 폭과 잦아드는 속도가 달라 어색해 보일 수 있습니다.

더 읽을거리

CSS 바이브 코딩 마이크로 인터랙션은 저장 체크 아이콘 하나가 그려지는 단일 마이크로 인터랙션을 다루는 반면, 이 편은 그 체크가 포함된 아홉 가지 저장 상태 값이 각각 어떻게 순환·전이하는지로 계산 축을 넓혀 다룹니다. 토스트 스택 UI 9종은 알림 여러 개가 쌓이고 밀려나는 위치 값을 계산하는 데 비해, 이 편은 저장이라는 한 가지 상태가 전이되는 순간의 값(펄스·비율·스프링)을 계산한다는 점에서 축이 다릅니다. 로그인 실패 횟수 제한 9종은 실패가 쌓일 때 시도 횟수·잠금 초 값이 줄어드는 계산을 다루는데, 이 편은 실패가 아니라 저장이라는 정상 흐름 위에서 값이 전이되는 순간의 계산이라는 점에서 다룹니다.

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

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