06손상 재시도 횟수 값 — 검증에 실패하면 재시도 횟수 값이 하나씩 늘고 진행 값이 처음으로 되돌아가는 방식
받은 파일 검증이 실패할 때마다 재시도 횟수 값이 하나씩 늘고, 진행 값은 0으로 돌아가 다시 올라갑니다.
쓰임불안정한 네트워크에서의 파일 다운로드, 무결성 검사가 있는 자료 배포
.dcr__retryBACKOFF_MULTVERIFY_SEED

<main class="stage">
<div class="dcr is-demo" id="dcr">
<div class="dcr__head">
<span class="dcr__state" id="dcrState">받는 중</span>
<span class="dcr__retry" id="dcrRetry">재시도 0</span>
</div>
<div class="dcr__track"><div class="dcr__fill" id="dcrFill"></div></div>
<button class="dcr__btn" id="dcrBtn" type="button">지금 재시도</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 재시도 대기 = 기저값 × 배수^재시도횟수(지수 백오프) — index.html backoffMs() 참조
@include stage($stage-yellow);
.dcr {
width: 100%;
max-width: 230px;
display: grid;
gap: $sp-3;
}
.dcr__head {
display: flex;
align-items: baseline;
justify-content: space-between;
font: 600 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $subject-ink;
}
.dcr--bad .dcr__state { color: $error; }
.dcr--ok .dcr__state { color: $subject-ink; }
.dcr__retry { font-variant-numeric: tabular-nums; color: rgba($subject-ink, .68); }
.dcr__track {
width: 100%;
height: $sp-3;
border-radius: $r-pill;
background: rgba($subject-ink, .14);
overflow: hidden;
}
.dcr__fill {
height: 100%;
width: 100%;
transform-origin: left center;
transform: scaleX(0);
border-radius: $r-pill;
background: $subject-ink;
transition: transform $dur-instant linear;
}
.dcr--bad .dcr__fill { background: $error; }
.dcr--ok .dcr__fill { background: $subject-mint; }
.dcr__btn {
width: 100%;
border: 0;
border-radius: $r-control;
padding: $sp-2 $sp-4;
background: $subject-ink;
color: $subject-cream;
font: 700 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform $dur-instant $ease-out;
&:active { transform: scale(.96); }
}
@media (max-width: 340px) {
.dcr { max-width: 195px; }
}
@media (prefers-reduced-motion: reduce) {
.dcr__fill, .dcr__btn { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.dcr {
width: 100%;
max-width: 230px;
display: grid;
gap: 12px;
}
.dcr__head {
display: flex;
align-items: baseline;
justify-content: space-between;
font: 600 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #17141a;
}
.dcr--bad .dcr__state {
color: #c2361a;
}
.dcr--ok .dcr__state {
color: #17141a;
}
.dcr__retry {
font-variant-numeric: tabular-nums;
color: rgba(23, 20, 26, 0.68);
}
.dcr__track {
width: 100%;
height: 12px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.14);
overflow: hidden;
}
.dcr__fill {
height: 100%;
width: 100%;
transform-origin: left center;
transform: scaleX(0);
border-radius: 999px;
background: #17141a;
transition: transform 140ms linear;
}
.dcr--bad .dcr__fill {
background: #c2361a;
}
.dcr--ok .dcr__fill {
background: #4cd4a6;
}
.dcr__btn {
width: 100%;
border: 0;
border-radius: 12px;
padding: 8px 16px;
background: #17141a;
color: #fff7e6;
font: 700 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform 140ms ease-out;
}
.dcr__btn:active {
transform: scale(0.96);
}
@media (max-width: 340px) {
.dcr {
max-width: 195px;
}
}
@media (prefers-reduced-motion: reduce) {
.dcr__fill, .dcr__btn {
transition: none;
}
}(function () {
var root = document.getElementById('dcr');
var state = document.getElementById('dcrState');
var retryEl = document.getElementById('dcrRetry');
var fill = document.getElementById('dcrFill');
var btn = document.getElementById('dcrBtn');
// 시드 고정: 회차별 검증 결과(참=검증 통과). 0·1회차는 손상, 2회차부터 통과
var VERIFY_SEED = [false, false, true, true, true];
// 상수: 틱 간격 · 기저 대기(ms) · 지수 배수 — 재시도할 때마다 대기 시간이 늘어난다
var TICK_MS = 90;
var BASE_BACKOFF_MS = 260;
var BACKOFF_MULT = 2;
var MAX_PCT = 100;
var pct = 0;
var retry = 0;
var timer = null;
var btnDisabled = false;
// 계산 겹: 재시도 대기 = 기저값 × 배수^재시도횟수(지수 백오프) — 매 실패마다 대기가 길어진다
function backoffMs() {
return BASE_BACKOFF_MS * Math.pow(BACKOFF_MULT, Math.min(retry, 4));
}
function paint() {
fill.style.transform = 'scaleX(' + (pct / MAX_PCT).toFixed(3) + ')';
retryEl.textContent = '재시도 ' + retry;
}
function verify() {
var ok = VERIFY_SEED[retry % VERIFY_SEED.length];
if (ok) {
state.textContent = '완료';
root.classList.add('dcr--ok');
setTimeout(reset, 900);
} else {
state.textContent = '손상';
root.classList.add('dcr--bad');
retry++;
pct = 0;
paint();
var wait = backoffMs();
timer = setTimeout(function () {
root.classList.remove('dcr--bad');
state.textContent = '받는 중';
download();
}, wait);
}
}
function download() {
if (pct >= MAX_PCT) { verify(); return; }
pct = Math.min(MAX_PCT, pct + 9);
paint();
timer = setTimeout(download, TICK_MS);
}
function reset() {
root.classList.remove('dcr--ok', 'dcr--bad');
pct = 0; retry = 0; state.textContent = '받는 중';
paint();
download();
}
download();
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
if (timer) clearTimeout(timer);
reset();
});
})();import { useEffect, useRef } from "react";
import styles from "./DownloadManagerCorruptRetry.module.scss";
type Box<T> = { current: T };
export default function DownloadManagerCorruptRetry() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const state = root.querySelector(`.${styles.state}`) as HTMLElement | null;
const retryEl = root.querySelector(`.${styles.retry}`) as HTMLElement | null;
const fill = root.querySelector(`.${styles.fill}`) as HTMLElement | null;
const btn = root.querySelector(`.${styles.btn}`) as HTMLElement | null;
if (!state || !retryEl || !fill || !btn) return;
// 시드 고정: 회차별 검증 결과(참=검증 통과). 0·1회차는 손상, 2회차부터 통과
const VERIFY_SEED = [false, false, true, true, true];
const TICK_MS = 90;
const BASE_BACKOFF_MS = 260;
const BACKOFF_MULT = 2;
const MAX_PCT = 100;
let pct = 0;
let retry = 0;
let timer = 0;
let stopped = false;
// 계산 겹: 재시도 대기 = 기저값 × 배수^재시도횟수(지수 백오프) — 매 실패마다 대기가 길어진다
function backoffMs() {
return BASE_BACKOFF_MS * Math.pow(BACKOFF_MULT, Math.min(retry, 4));
}
function paint() {
fill!.style.transform = "scaleX(" + (pct / MAX_PCT).toFixed(3) + ")";
retryEl!.textContent = "재시도 " + retry;
}
function verify() {
const ok = VERIFY_SEED[retry % VERIFY_SEED.length];
if (ok) {
state!.textContent = "완료";
root!.classList.add(styles["dcr--ok"]);
timer = window.setTimeout(reset, 900);
} else {
state!.textContent = "손상";
root!.classList.add(styles["dcr--bad"]);
retry++;
pct = 0;
paint();
const wait = backoffMs();
timer = window.setTimeout(() => {
root!.classList.remove(styles["dcr--bad"]);
state!.textContent = "받는 중";
download();
}, wait);
}
}
function download() {
if (stopped) return;
if (pct >= MAX_PCT) { verify(); return; }
pct = Math.min(MAX_PCT, pct + 9);
paint();
timer = window.setTimeout(download, TICK_MS);
}
function reset() {
root!.classList.remove(styles["dcr--ok"], styles["dcr--bad"]);
pct = 0; retry = 0; state!.textContent = "받는 중";
paint();
download();
}
download();
function onClick() {
root!.classList.remove(styles["is-demo"]);
window.clearTimeout(timer);
reset();
}
btn.addEventListener("click", onClick);
return () => {
stopped = true;
window.clearTimeout(timer);
btn.removeEventListener("click", onClick);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.dcr} ${styles["is-demo"]}`}>
<div className={styles.head}>
<span className={styles.state}>받는 중</span>
<span className={styles.retry}>재시도 0</span>
</div>
<div className={styles.track}><div className={styles.fill} /></div>
<button className={styles.btn} type="button">지금 재시도</button>
</div>
);
}06 손상 재시도 횟수 값 — 검증에 실패하면 재시도 횟수 값이 하나씩 늘고 진행 값이 처음으로 되돌아가는 방식 — 측정 누적면적 2.188% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 다운로드 진행 상황 표시 9종 — 값이 퍼센트 단위로 갱신되는 아홉 가지 방식
이 데모가 속한 글
다운로드 진행 상황 표시 9종 — 값이 퍼센트 단위로 갱신되는 아홉 가지 방식다운로드 진행 상황 표시는 지금 받아지는 파일이 어느 상태에 있는지를 값 하나로 계산해 화면에 드러내는 인터랙션을 말하는데, 아홉 개 중 하나는 여러 파일이 동시에 받아질 때 각 파일의 진행 값이 목록에서 나란히 오 이 글의 데모 전부 보기

