새로고침해도 안 바뀌는 '랜덤'이 필요할 때
Math.sin 해시 기반 seeded pseudo-random
난이도: ★★★☆☆ 연관 노트: 서버와 브라우저의 ‘오늘’은 다르다
핵심 요약
Math.random()은 호출할 때마다 계산된다. 인자가 없어서 새로고침하면 이전과 다른 값을 반환한다.Math.sin은 인자 기준으로 결과를 반환하기에, 인자만 똑같으면 언제 어디서 호출해도 동일한 결과를 반환한다.- 그래서 “날짜”를 인자(시드)로 넣으면 오늘의 고정된 랜덤이 된다 — 새로고침해도, 다른 기기에서 봐도 같은 값.
왜 헷갈렸나
Math.random()과 Math.sin 해시를 “택 1 해도 되는 동급 선택지”라고 생각했다. 그리고 sin을 쓰는 이유를 “중복이 안 생겨서”라고 짚었다.
둘 다 틀렸다. 중복 방지는 핵심이 아니고, Math.random()은 이 요구사항(새로고침해도 같은 숫자)을 아예 만족할 수 없다. 시드를 지정할 방법이 없기 때문이다.
메커니즘
요구사항: 가입자 수 카운터가 매일 자연스럽게 다른 폭으로 증가하되, 같은 날짜면 모든 사용자·새로고침·기기에서 같은 숫자여야 한다.
Math.random() Math.sin(dayIndex)
───────────── ──────────────────
호출 1: 0.7231... sin(1) = 0.8414... (영원히)
호출 2: 0.1892... ← 매번 다름 sin(2) = 0.9092... (영원히)
호출 3: 0.5547... sin(3) = 0.1411... (영원히)
새로고침 → 숫자 널뛰기 💥 새로고침 → 같은 숫자 ✓
유저 A ≠ 유저 B 💥 유저 A = 유저 B ✓
Math.sin이 해시 함수 노릇을 할 수 있는 성질 3가지:
- 순수 함수 — 같은 입력이면 언제나 같은 출력
- 예측 불가능해 보이는 출력 — 입력이 1, 2, 3으로 규칙적이어도 출력은 0.84 → 0.91 → 0.14로 널뛴다
- 긴 소수부 —
× 10000으로 소수점을 끌어내면 사실상 무작위처럼 보이는 수가 나온다
해결 패턴
/** 날짜 인덱스 기반 결정적(deterministic) 일별 증가량 (3~30) */
const getDailyIncrement = (dayIndex: number) => {
// sin 결과(-1~1)에 10000을 곱해 소수부를 정수부로 끌어냄
const hash = Math.abs(Math.sin(dayIndex + 1) * 10000);
// % 28 → 0~27, +3 → 3~30 범위
return 3 + Math.floor(hash % 28);
};
const getCount = (now: Date) => {
const elapsedDays = Math.max(
0,
Math.floor((now.getTime() - new Date(BASE_DATE).getTime()) / 86_400_000),
);
let count = BASE_COUNT;
for (let i = 0; i < elapsedDays; i++) {
count += getDailyIncrement(i); // 같은 날짜 → 항상 같은 누적값
}
return count;
};
셰이더/게임 개발 쪽에서 오래 쓰인 트릭이다. 암호학적으로는 전혀 안전하지 않지만, “매일 그럴듯하게 다른 값” 용도로는 충분하다.
다음에 이 상황을 만나면
“랜덤인데 재현 가능해야 한다”는 요구가 보이면 → Math.random()은 후보에서 제외하고 시드 기반으로 간다.
- 이 값이 새로고침/다른 기기에서 같아야 하는가? → Yes면 seeded
- 시드로 쓸 안정적인 입력이 있는가? (날짜, ID, 인덱스)
- 보안 용도인가? → 그러면 sin 해시 금지,
cryptoAPI로
커넥팅 닷
← 선행 개념 (이걸 알아야 이해된다)
- 순수 함수(Pure Function) — “같은 입력 → 같은 출력”이 이 패턴의 전부다. sin은 순수, random은 비순수
- 해시 함수 — 입력을 예측 불가능해 보이는 고정 출력으로 매핑한다는 개념. sin을 싸구려 해시로 쓴 것
→ 확장 개념 (여기서 더 나아가면)
- PRNG(의사 난수 생성기)와 시드 — Mulberry32, xorshift 등 진짜 seeded PRNG. sin 해시의 정석 버전
crypto.getRandomValues()— 보안이 필요한 난수는 이쪽. Math 계열은 절대 금지
↔ 같은 원리가 적용되는 곳
- 서버와 브라우저의 ‘오늘’은 다르다 — 같은 기능(카운터)에서 만난 문제. 결정적 계산이어도 “입력(날짜)“이 서버/클라에서 다르면 결과가 갈린다
- React의 렌더 순수성 — 렌더 함수도 “같은 props/state → 같은 JSX”를 요구한다. 렌더 중
Math.random()호출이 안티패턴인 이유와 동일
참고
- MDN — Math.random() — 시드 지정 불가 명시