Math.random()으로 만든 id가 충돌해서 목록이 뒤섞인 이유
문제 발생
임시 id를 만들려고 흔히 쓰이는 이 코드를 썼는데, 목록이 커지자 같은 id가 두 번 나와 React 리스트가 뒤섞였습니다.
const id = Math.random().toString(36).slice(2, 9); // 7자세션 토큰에도 비슷한 방식을 쓰고 있다는 걸 나중에 발견했습니다.
원인 분석
문제는 두 가지이고 심각도가 다릅니다.
충돌. 위 코드는 36진수 7자리이므로 경우의 수가 36⁷ ≈ 780억입니다. 커 보이지만 생일 문제 때문에 수만 개만 만들어도 충돌 확률이 무시할 수 없는 수준이 됩니다. 잘라 쓸수록 급격히 나빠집니다.
예측 가능성. Math.random()은 암호학적으로 안전하지 않습니다. 빠른 난수 생성이 목적이라 내부 상태를 추정할 수 있고, 출력 몇 개를 관찰하면 다음 값을 예측할 수 있는 구현이 존재합니다. 세션 토큰이나 비밀번호 재설정 링크에 쓰면 그 자체가 취약점입니다.
crypto.randomUUID()는 암호학적으로 안전한 난수 생성기로 v4 UUID 36자 문자열을 만듭니다. 2022년 3월부터 Baseline입니다.
해결 방안
- id가 필요하면
crypto.randomUUID()를 씁니다.
const id = crypto.randomUUID();
// "36b8f84d-df4e-4d49-b662-bcde71a8764f"- 보안 컨텍스트가 필요하다는 점을 압니다. HTTPS(그리고
localhost)에서만 사용할 수 있습니다.http://로 접근하는 개발 서버에서는crypto.randomUUID is not a function이 납니다 — 브라우저가 오래돼서가 아니라 컨텍스트 때문입니다. - 길이나 형식이 달라야 하면
getRandomValues를 씁니다. 같은 안전한 난수원을 쓰되 형식만 바꿉니다.
const bytes = crypto.getRandomValues(new Uint8Array(16));
const token = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");- 잘라 쓰지 않습니다. UUID가 길다고 앞 8자만 쓰면 위의 충돌 문제로 되돌아갑니다. 짧은 식별자가 필요하면 DB의 시퀀스처럼 유일성을 보장하는 출처를 쓰는 편이 맞습니다.
- React의 key에는 데이터의 id를 씁니다. 렌더 중에 만든 난수 id는 리렌더마다 달라져 컴포넌트가 통째로 다시 만들어집니다. 서버가 준 id가 없다면 목록을 만들 때 한 번만 부여하고 상태에 보관합니다.
- DOM 속성용 id라면 React의
useId가 맞습니다. 서버와 클라이언트에서 같은 값이 나와야 하이드레이션이 어긋나지 않는데, 난수는 그 조건을 만족하지 못합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.