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

AI 로딩 애니메이션 9가지 — 값 하나로 도는 처리중 표시

궤도 반경 값이 크면 입자가 넓게 흩어지는데 값이 0에 가까워지면 코어로 모이고, 그 사이 후광 링의 색상 값은 완료 색 쪽으로 넘어가며, 토큰 순번 값이 오를 때마다 미리보기 글자가 하나씩 늘어난다. 이 아홉 가지 ai 로딩 애니메이션도 값 하나로 이 흐름을 그대로 재현합니다.

이 글은 이런 하나짜리 계산값을 UI 로 바꿔 보여주는 아홉 가지 AI 로딩 애니메이션을 값 계산 방식별로 나눠 놓았습니다. 아홉 개 모두 requestAnimationFrame 루프 안에서 값 하나만 갱신하고, 클릭이나 Space·Enter 로 멈췄다 다시 움직일 수 있고 R 로 처음 값으로 되돌아갑니다. 창 크기가 바뀌면 resize 리스너가 같은 값으로 즉시 다시 그려 레이아웃이 흐트러지지 않게 막습니다.

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

AI 로딩 중 표시는 채팅 로딩·버튼 로딩과 이 지점에서 갈라집니다

AI 채팅 CSS 로딩 애니메이션 9종 은 말풍선 안에서 점이나 커서가 움직이는 대화창 전용 표시이고, 버튼 로딩 9종 — 스피너부터 성공 체크까지 상태 전환 은 클릭 이후 버튼 하나의 상태(대기→성공/실패) 전환에 집중합니다. 이 글의 아홉 개는 대화창이나 버튼 안에 갇히지 않고, 카드·배지·전체 화면 어디에도 붙일 수 있는 '지금 계산 중'이라는 신호 자체를 값 하나로 표현합니다.

캔버스 로딩 애니메이션 효과 9가지 — 코드 그대로 가 중력이나 충돌 같은 물리 값을 계산해 넣는 쪽이라면, 이번 아홉 개 중에서는 03·04 두 개만 캔버스를 쓰고 나머지 일곱 개는 CSS 커스텀 프로퍼티나 SVG path 값을 직접 갱신합니다. 힘을 시뮬레이션하지 않고 hue·반경·지연·진폭처럼 축 하나의 값만 시간에 따라 계산한다는 점이 이 아홉 개를 가장 단순하게 만듭니다.

가짜 처리 중 표시라는 함정에 빠지지 않으려면(사용자 기대 관리)

아홉 개 모두 LOOP 초 단위로 반복되는 고정 애니메이션이며, 실제 모델이나 API 호출과는 연결돼 있지 않습니다. 화면에 뜨는 '처리 중', '듣는 중' 같은 문구도 미리 박아 둔 고정 텍스트일 뿐이라 어떤 서비스명이나 모델명도 담지 않았습니다.

문제는 실제 서비스에 붙일 때입니다. 진짜 완료 신호(응답 도착·스트림 종료)를 연결하지 않고 이 값 계산만 그대로 두면, 실제 처리가 이미 끝났는데도 화면은 LOOP 값을 따라 계속 돌아 사용자에게는 ai 무한로딩처럼 보일 수 있습니다. 반대로 실제 처리가 LOOP 값보다 오래 걸리면 애니메이션은 진작 한 바퀴를 돌고도 아무 일 없다는 듯 같은 리듬을 반복해, "멈춘 건가" 하는 의심을 사기 쉽습니다.

그래서 이 아홉 개는 장식이 아니라 신호로 써야 합니다 — 실제 완료 이벤트가 오는 즉시 애니메이션을 멈추고 결과 화면으로 바꿔치기하는 코드를 여러분이 직접 붙여야, 값과 실제 상태가 어긋나는 함정을 피할 수 있습니다.

01색상 값이 완료 쪽으로 도는 후광 링 AI 로딩

conic-gradient 로 그린 후광 링이 회전하며 처리 진행 값에 따라 색상(hue) 값을 완료 색 쪽으로 서서히 옮겨, 링 색만 보고도 얼마나 끝나가는지 가늠할 수 있습니다. 클릭이나 Space·Enter 로 일시정지를 토글하고 R 로 재시작할 수 있으며, 루프 한 프레임마다 hue 값 하나만 다시 계산합니다.

색상 값이 완료 쪽으로 도는 후광 링 AI 로딩
새 창에서 열기

01 · 색상 값이 완료 쪽으로 도는 후광 링 AI 로딩 — 측정 누적면적 22.139% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

conic-gradientAI_LOADER_HUE_TRANSITION_MSROTATE_SPEED
  function draw(sec) {
    var angle = (sec * ROTATE_SPEED) % 360;
    var p = (sec % LOOP) / LOOP;
    var winStart = 1 - (AI_LOADER_HUE_TRANSITION_MS / 1000) / LOOP;
    var mixT = p <= winStart ? 0 : smooth((p - winStart) / (1 - winStart));
    var col = mix(BLUE, DONE, mixT);
    var col2 = mix(BLUE, DONE, mixT * 0.6);

    ring.style.setProperty('--angle', angle + 'deg');
    ring.style.setProperty('--c1', rgbStr(col, 1));
    ring.style.setProperty('--c2', rgbStr(col2, .35));
    var corePulse = 1 + 0.08 * Math.sin(sec * Math.PI * 2 / LOOP * 3);
    core.style.setProperty('--core-scale', corePulse.toFixed(3));
    core.style.setProperty('--core-c', rgbStr(col, 1));

    var pct = Math.round(p * 100);
    pctEl.textContent = pct + '%';
  }

02대기 시간 값이 늘어날수록 숨쉬듯 커지는 오브 펄스

대기 시간 값을 사인 이징 곡선에 넣어 중심 오브의 크기와 블러 값을 천천히 키웠다 줄였다 반복시키고, 값이 오래 쌓일수록 숨쉬는 폭 자체가 조금씩 커집니다. 숨쉬는 폭은 사인 곡선 하나에 이징을 얹은 값이라, 대기 시간이 늘어도 오브가 갑자기 튀지 않고 폭만 서서히 커집니다.

대기 시간 값이 늘어날수록 숨쉬듯 커지는 오브 펄스
새 창에서 열기

02 · 대기 시간 값이 늘어날수록 숨쉬듯 커지는 오브 펄스 — 측정 누적면적 30.089% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BREATH_HZAMP_BASEAMP_GROW
  function draw(sec) {
    var t = sec % LOOP;
    var accum = Math.min(sec, LOOP) / LOOP;      // 대기 시간 값이 쌓인 정도(0~1)
    var amp = AMP_BASE + AMP_GROW * easeSine(accum);
    var wave = Math.sin(t * Math.PI * 2 * BREATH_HZ);
    var scale = 1 + amp * wave;
    var blur = 12 + 9 * Math.max(0, wave);
    var glow = 0.35 + 0.25 * Math.max(0, wave);

    orb.style.setProperty('--scale', scale.toFixed(4));
    orb.style.setProperty('--blur', blur.toFixed(1) + 'px');
    orb.style.setProperty('--glow', glow.toFixed(3));

    var pct = Math.round((t / LOOP) * 100);
    pctEl.textContent = pct + '%';
  }

03궤도 반경 값이 좁아지며 코어에 모이는 입자 소용돌이

코어 둘레를 도는 입자마다 자신만의 궤도 반경 값을 갖고, 처리 진행 값이 오를수록 그 반경 값이 함께 줄어들어 입자들이 점점 코어 쪽으로 모여듭니다. 궤도 반경은 이징 지수 하나로만 줄어들기 때문에, AI_LOADER_ORBIT_SHRINK_EASE 값을 바꾸면 입자가 모이는 리듬 전체가 달라집니다.

궤도 반경 값이 좁아지며 코어에 모이는 입자 소용돌이
새 창에서 열기

03 · 궤도 반경 값이 좁아지며 코어에 모이는 입자 소용돌이 — 측정 누적면적 25.456% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

AI_LOADER_ORBIT_SHRINK_EASEGOLDENMAX_R
    ctx.fillStyle = TRAIL;
    ctx.fillRect(0, 0, W, H);

    var cx = W / 2, cy = H / 2;
    var p = (sec % LOOP) / LOOP;
    var shrink = Math.pow(p, 1 / AI_LOADER_ORBIT_SHRINK_EASE);   // 궤도 반경 값 축소 이징

    for (var i = 0; i < particles.length; i++) {
      var q = particles[i];
      var angle = q.baseAngle + sec * q.speed;
      var r = CORE_R + (MAX_R - CORE_R) * (1 - shrink) * q.jitter;
      var x = cx + r * Math.cos(angle);
      var y = cy + r * Math.sin(angle) * 0.86;
      ctx.beginPath();
      ctx.arc(x, y, q.size, 0, Math.PI * 2);
      ctx.fillStyle = i % 2 === 0 ? DOT : DOT_DEEP;
      ctx.shadowColor = DOT;
      ctx.shadowBlur = 10;
      ctx.fill();
      ctx.shadowBlur = 0;
    }

    var coreR = CORE_R * (0.85 + 0.3 * (1 - shrink));

04지연 값을 따라 마디를 건너가는 신경망 펄스

작은 노드와 선으로 만든 격자 위에서, 각 선마다 다른 지연(delay) 값을 줘 빛 신호가 마디에서 마디로 순서대로 건너가듯 퍼지게 합니다. 선마다 지연 값이 다르기 때문에 신호가 왼쪽 위에서 오른쪽 아래로 퍼지는 것처럼 보이지만, 실제로는 좌표 합만으로 지연 순서를 정한 값 계산입니다.

지연 값을 따라 마디를 건너가는 신경망 펄스
새 창에서 열기

04 · 지연 값을 따라 마디를 건너가는 신경망 펄스 — 측정 누적면적 11.583% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STEPEDGE_MSPULSE_W
  var nodes = [];
  for (var r = 0; r < ROWS; r++) {
    for (var c = 0; c < COLS; c++) {
      var dist = r + c;                              // 원점에서 이 마디까지 격자 거리
      nodes.push({ r: r, c: c, delay: dist * STEP + seeded(r * COLS + c) * 0.04 });
    }
  }
  function idx(r, c) { return r * COLS + c; }
  var edges = [];
  for (var r2 = 0; r2 < ROWS; r2++) {
    for (var c2 = 0; c2 < COLS; c2++) {
      if (c2 < COLS - 1) edges.push({ a: idx(r2, c2), b: idx(r2, c2 + 1) });
      if (r2 < ROWS - 1) edges.push({ a: idx(r2, c2), b: idx(r2 + 1, c2) });
    }
  }
  for (var e = 0; e < edges.length; e++) {
    var ea = nodes[edges[e].a], eb = nodes[edges[e].b];
    edges[e].delay = Math.min(ea.delay, eb.delay);
  }
  var maxDelay = 0;
  for (var m = 0; m < nodes.length; m++) if (nodes[m].delay > maxDelay) maxDelay = nodes[m].delay;

05채워진 줄 수 값이 늘어나는 결과 카드 스트림 로딩

결과 카드 안의 스켈레톤 줄마다 채워졌는지 여부를 값으로 두고, 그 값이 위에서부터 하나씩 참으로 바뀔 때마다 해당 줄만 실제 텍스트 톤으로 바뀝니다. 값은 위에서부터 순서대로 바뀌고, 줄마다 채워진 비율이 별도 값으로 관리되어 완료된 줄만 실제 텍스트 색으로 넘어갑니다.

채워진 줄 수 값이 늘어나는 결과 카드 스트림 로딩
새 창에서 열기

05 · 채워진 줄 수 값이 늘어나는 결과 카드 스트림 로딩 — 측정 누적면적 14.256% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LINE_MSFILL_MS--skeleton
  function draw(sec) {
    var t = sec % LOOP;
    var doneCount = 0;
    for (var i = 0; i < lines.length; i++) {
      var local = t - i * LINE_MS;
      var raw = clamp01(local / FILL_MS);
      var eased = smooth(raw);
      fills[i].style.width = (eased * 100).toFixed(1) + '%';
      fills[i].style.background = rgbStr(mix(SKELETON, TEXT, eased));
      lines[i].classList.toggle('is-active', raw > 0 && raw < 1);
      if (raw >= 1) doneCount++;
    }
    pctEl.textContent = Math.round((doneCount / lines.length) * 100) + '%';
  }

06완료 값이 뒤집히면 반짝임으로 바뀌는 버튼 아이콘

버튼 안 아이콘의 path 값을 스피너 모양에서 반짝임(스파클) 모양으로 미리 정해 두고, 완료 값이 false 에서 true 로 바뀌는 순간 그 path 값만 보간해 모양을 바꿉니다. 완료 값이 뒤집히는 순간 path 좌표 여덟 쌍이 동시에 보간되어 스피너 모양이 스파클 모양으로 바뀝니다.

완료 값이 뒤집히면 반짝임으로 바뀌는 버튼 아이콘
새 창에서 열기

06 · 완료 값이 뒤집히면 반짝임으로 바뀌는 버튼 아이콘 — 측정 누적면적 10.111% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pathDONEMORPH_MS
    var t = sec % LOOP;
    var localFlip = t - FLIP_AT;

    if (localFlip >= 0) { DONE = true; } else { DONE = false; }

    var morphT = DONE ? smooth(clamp01(localFlip / MORPH_MS)) : 0;
    var radii = [];
    for (var i = 0; i < 8; i++) {
      var spinnerR = R_RING;
      var sparkleR = (i % 2 === 0) ? R_OUT : R_IN;
      radii[i] = spinnerR + (sparkleR - spinnerR) * morphT;
    }
    var angleOffset = (sec * ROT_SPEED) % 360 * Math.PI / 180;
    var rot = DONE ? angleOffset * 0.12 : angleOffset;
    path.setAttribute('d', buildPath(radii, rot));

    var pulse = 1;
    if (DONE && localFlip < 0.5) {
      pulse = 1 + 0.22 * Math.sin(clamp01(localFlip / 0.5) * Math.PI);
    }
    path.setAttribute('transform', 'scale(' + pulse.toFixed(3) + ')');

07입력 크기 값이 막대 높이를 흔드는 듣는 중 파형

마이크 입력 크기를 흉내 낸 값을 막대마다 다르게 주고, 그 값이 오르내릴 때마다 막대 높이가 따라 흔들려 지금 소리를 듣고 있는 상태를 보여줍니다. 실제 마이크 입력을 받는 게 아니라 막대마다 다른 두 사인 값을 합성해 흔드는 값이라, 진짜 소리와는 무관하게 항상 같은 리듬으로 움직입니다.

입력 크기 값이 막대 높이를 흔드는 듣는 중 파형
새 창에서 열기

07 · 입력 크기 값이 막대 높이를 흔드는 듣는 중 파형 — 측정 누적면적 17.854% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

seededfreqABASE_H
  function draw(sec) {
    var t = sec;
    for (var i = 0; i < N; i++) {
      var q = bars[i];
      var env = 0.5 + 0.5 * Math.sin(t * q.freqA * Math.PI * 2 + q.phase);
      var jit = Math.sin(t * q.freqB * Math.PI * 2 + q.phase * 1.7) * 0.22;
      var level = clamp01(env + jit);
      var h = BASE_H + (MAX_H - BASE_H) * level;
      bels[i].style.height = h.toFixed(1) + 'px';
      bels[i].style.opacity = (0.55 + level * 0.45).toFixed(2);
    }
    var avg = 0;
    for (var j = 0; j < N; j++) avg += parseFloat(bels[j].style.height);
    avg = Math.round((avg / N - BASE_H) / (MAX_H - BASE_H) * 100);
    pctEl.textContent = avg > 8 ? '듣는 중' : '대기';
  }

08토큰 순번 값이 오를 때마다 한 글자씩 드러나는 고스트 텍스트

미리 정해 둔 결과 문장을 글자 배열로 쪼개 두고, 토큰 순번 값이 하나씩 올라갈 때마다 그 순번까지의 글자만 옅은 고스트 색에서 실제 색으로 바뀌며 캐럿이 그 뒤를 따라갑니다. 토큰 순번 값이 하나 오를 때마다 그 순번까지의 글자 색만 다시 계산되고, 캐럿은 마지막으로 드러난 글자의 위치 값을 따라갑니다.

토큰 순번 값이 오를 때마다 한 글자씩 드러나는 고스트 텍스트
새 창에서 열기

08 · 토큰 순번 값이 오를 때마다 한 글자씩 드러나는 고스트 텍스트 — 측정 누적면적 46.62% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

AI_LOADER_TOKEN_REVEAL_MSFADE_MSBLINK_MS
  function draw(sec) {
    if (!GHOST) readColors();
    var t = sec % LOOP;
    var elapsedMs = t * 1000;
    var counter = Math.max(0, Math.min(LEN, Math.floor(elapsedMs / AI_LOADER_TOKEN_REVEAL_MS)));

    for (var i = 0; i < LEN; i++) {
      var revealAt = i * AI_LOADER_TOKEN_REVEAL_MS;
      var fade = clamp01((elapsedMs - revealAt) / FADE_MS);
      els[i].style.color = rgbStr(mix(GHOST, REAL, fade));
    }

    var target = counter < LEN ? els[counter] : els[LEN - 1];
    if (target) {
      var left = counter < LEN ? target.offsetLeft : target.offsetLeft + target.offsetWidth;
      var top = target.offsetTop;
      caret.style.transform = 'translate(' + left.toFixed(1) + 'px,' + top.toFixed(1) + 'px)';
      fill.style.width = left.toFixed(1) + 'px';
    }
    var blinkOn = Math.floor(elapsedMs / BLINK_MS) % 2 === 0;
    caret.style.opacity = blinkOn ? '1' : '.15';
  }

09동작 줄임 값을 감지하면 즉시 문구로 바뀌는 처리중 배지

시스템의 동작 줄임 설정 값을 감지해, 그 값이 켜져 있으면 위 여덟 패턴 대신 애니메이션 없이 '처리 중' 문구 배지가 바로 나타나고, 꺼져 있을 때만 원래 속도로 움직입니다. 동작 줄임이 꺼져 있을 때는 점 세 개가 사인 값으로 튀어 오르고, 켜지는 순간에는 스프링 감쇠 값 하나로 배지가 한 번만 튀어 오른 뒤 멈춥니다.

동작 줄임 값을 감지하면 즉시 문구로 바뀌는 처리중 배지
새 창에서 열기

09 · 동작 줄임 값을 감지하면 즉시 문구로 바뀌는 처리중 배지 — 측정 누적면적 2.808% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SWITCH_ATspringBOUNCE_HZ
  function draw(sec) {
    var t = sec % LOOP;
    var on = t >= SWITCH_AT;   // 동작 줄임 값(시뮬레이션)

    if (on) {
      var localT = (t - SWITCH_AT) / SPRING_MS;
      var s = spring(localT);
      badge.style.opacity = '1';
      badge.style.transform = 'scale(' + (0.7 + 0.3 * s).toFixed(3) + ')';
      dots.length && (root.querySelector('.ld__dots').style.opacity = '0');
      stateEl.textContent = '동작 줄임: 켜짐 · 처리 중 배지';
    } else {
      badge.style.opacity = '0';
      badge.style.transform = 'scale(.7)';
      root.querySelector('.ld__dots').style.opacity = '1';
      for (var i = 0; i < dots.length; i++) {
        var phase = i * (Math.PI * 2 / 3);
        var y = Math.sin(t * BOUNCE_HZ * Math.PI * 2 + phase);
        dots[i].style.transform = 'translateY(' + (y * 7).toFixed(1) + 'px)';
        dots[i].style.opacity = (0.55 + 0.45 * (0.5 + 0.5 * y)).toFixed(2);
      }
      stateEl.textContent = '동작 줄임: 꺼짐 · 움직이는 중';
    }
  }

아홉 개 값이 실제로 얼마나 움직였는지 (23프레임 측정)

항목 누적면적 강도_평균 움직인프레임
01 후광 링 22.14% 30.8 23/23
02 오브 펄스 30.09% 40.9 23/23
03 입자 소용돌이 25.46% 60.7 23/23
04 신경망 펄스 11.58% 69.1 23/23
05 카드 스트림 14.26% 105.4 20/23
06 버튼 아이콘 스파클 10.11% 175.4 23/23
07 듣는 중 파형 17.85% 100.9 23/23
08 고스트 텍스트 46.62% 52.6 20/23
09 처리중 배지 2.81% 120.3 17/23

누적면적이 가장 큰 값은 08 고스트 텍스트(46.62%)로, 글자 배열 전체의 색이 매 프레임 다시 계산되기 때문입니다. 반대로 09 배지는 2.81%로 가장 작은데, 동작 줄임 값 하나만 보고 정지 프레임 근처에서 점 세 개만 작게 움직이는 정적 배지라 원래 면적이 작은 게 정상입니다 — 09를 억지로 키우지 않고 그대로 둔 이유이기도 합니다. 05·08 두 항목만 20/23 프레임에서 값이 고정 구간에 머물러 움직인프레임이 23 에 못 미칩니다.

실제 화면 어디에 이 아홉 개를 붙이면 좋을까

정적 ai 로딩 이미지 한 장만 걸어두는 것보다, 01 후광 링이나 08 고스트 텍스트처럼 값이 실제로 움직이는 쪽이 사용자에게 진행 신호를 더 분명히 전달합니다. 요약·정리·초안 생성처럼 몇 초 이상 걸리는 화면에서 결과 영역 위에 얹어 두면, 사용자가 새로고침을 누르기 전에 지금 계산 중이라는 값을 계속 보여줄 수 있습니다.

채팅형 인터페이스라면 03 입자 소용돌이로 입력을 받는 단계를, 07 파형으로 음성을 듣는 단계를 서로 다른 항목으로 나눠 붙이는 편이 자연스럽고, 카드형 목록에서 항목 하나씩 결과가 채워지는 화면이라면 05 스트림 로딩이 줄 단위 진행을 그대로 보여줍니다.

동작 줄임 설정을 켠 사용자에게는 09 배지처럼 애니메이션 없이 문구만 바뀌는 쪽을 기본값으로 깔아 두는 게 안전합니다.

값 하나만 바꿔서 느낌이 어떻게 달라지는지 확인해보세요 (변형 3종)

아래 세 값만 각각 바꿔서 재생해보면 같은 아홉 개 구조 안에서도 느낌이 크게 갈립니다.

이름 바꾼 값 느낌
후광 링 색상 전환 속도 값 조정 01 AI_LOADER_HUE_TRANSITION_MS 640 → 260 값을 짧게 줄이면 진행 값이 살짝만 올라도 색이 훅 넘어가 완료 직전처럼 보이고, 값을 늘리면 색이 거의 끝까지 가야 넘어가 여유 있게 느껴져야 합니다.
입자 궤도 반경 축소 이징 값 조정 03 AI_LOADER_ORBIT_SHRINK_EASE 2.2 → 4.8 이징 값을 선형에 가깝게 두면 입자가 일정한 속도로 코어에 모여드는 느낌이고, 값을 급격한 곡선으로 바꾸면 초반엔 거의 안 움직이다 막판에 확 모여드는 쪽에 가까워집니다.
고스트 텍스트 토큰 노출 간격 값 조정 08 AI_LOADER_TOKEN_REVEAL_MS 62 → 26 간격 값을 짧게 줄이면 글자가 거의 한꺼번에 쏟아지듯 나와야 하고, 값을 늘리면 한 글자씩 또박또박 읽히는 속도에 가까워집니다.

값을 코드에서 직접 바꿔보며 아홉 개 원본 파일을 전부 손에 쥐고 싶다면 379w2wxy 을 입력해 압축을 풀면 됩니다.

접근성 — 동작 줄임을 켜면 아홉 개 모두 값 하나로 멈춥니다

아홉 항목 모두 prefers-reduced-motion 값이 reduce 이면 requestAnimationFrame 루프를 아예 시작하지 않고, 진행률 62% 지점(LOOP * 0.62)에 해당하는 프레임 한 장만 draw() 로 그려 넣고 멈춥니다. 09 배지만 예외로, 스프링이 끝난 시점(SWITCH_AT + SPRING_MS)의 정지 화면을 한 번 그립니다.

실험판 요소마다 role="application" 값과 tabindex="0" 값이 들어 있어 Tab 으로 초점을 옮길 수 있고, 항목마다 다른 aria-label 값을 읽어 줘 스크린리더 사용자도 지금 어떤 로딩인지 구분할 수 있습니다. 접근성 값은 코드에 이미 반영돼 있어 별도 설정 없이 시스템 설정만 따라갑니다.

자주 묻는 질문 (FAQ)

이 로딩 애니메이션이 실제 AI 처리 속도와 맞아떨어지나요?

아니요. 아홉 개 모두 LOOP 초 단위로 정해진 고정 시간 동안 반복되는 값이라, 실제 서버 응답이 그보다 빠르거나 늦어도 애니메이션 자체는 같은 리듬으로 돕니다. 진짜 완료 신호는 여러분의 코드에서 API 응답을 받는 시점에 별도로 연결해야 합니다.

항목마다 값을 바꾸면 파일도 따로 받아야 하나요?

아니요. 값 하나씩 실험해보고 싶다면 위쪽 변형 표 옆에서 압축 파일을 받아 아홉 개 HTML·CSS·JS 를 로컬에서 직접 열어보면 됩니다.

ai 로딩 중 상태가 화면 어디에 있는지 사용자가 못 찾으면 어떻게 하나요?

role="application" 값과 tabindex="0" 값이 실험판 요소에 들어 있어 Tab 으로 초점을 옮길 수 있고, aria-label 값이 항목마다 다른 이름을 읽어 줘 스크린리더 사용자도 지금 어떤 로딩인지 구분할 수 있습니다.

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

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