내부 문서용 단축링크를 6자리로 만들 때 정한 것들

정적 사이트에 서버 없이 단축링크를 붙였습니다. 알파벳에서 헷갈리는 글자를 빼고, 조합 수와 충돌 확률을 계산해 6자리로 정한 과정입니다.

사내 문서 주소는 길고 잘 바뀝니다. 회의 중에 주소를 부르거나 화이트보드에 적어야 할 때가 있어서, 짧은 코드로 대신 부를 수단이 필요했습니다. 이 사이트에 /s/<코드>/ 형태의 단축링크를 붙이면서 정한 것들을 적어 둡니다.

알파벳에서 무엇을 뺐나

사람이 옮겨 적는 순간 0o, 1l 은 같은 글자가 됩니다. 둘 다 빼면 알파벳이 크게 줄고, 둘 다 남기면 오타가 납니다. 그래서 짝에서 한쪽만 남겼습니다. 숫자를 남기고 글자를 뺐습니다. 손으로 쓴 글자가 숫자보다 더 자주 오인되기 때문입니다.

  • o 제거 → 0 만 남김
  • l 제거 → 1 만 남김
  • i 제거 → 1·l 과 섞이는 세 번째 후보라 함께 뺌
  • u 제거 → v 와 혼동되고, 우연히 비속어가 만들어지는 조합을 줄임

남은 알파벳은 숫자 10자와 글자 22자를 더해 32자입니다. Crockford Base32 와 같은 구성이고, 32는 2의 5제곱이라 코드 한 글자가 정확히 5비트에 대응합니다.

const shortlinkAlphabet = "0123456789abcdefghjkmnpqrstvwxyz"; // 32자

조합은 몇 개인가

32자로 6자리를 만들면 32의 6제곱, 즉 1,073,741,824개(약 10억 7천만)입니다. 코드 하나가 30비트 정보량입니다.

길이조합 수정보량느낌
4자리1,048,57620비트100만 개. 내부용으로도 빠듯
5자리33,554,43225비트3,300만 개. 충분하지만 여유가 적음
6자리1,073,741,82430비트10.7억 개. 채택
7자리34,359,738,36835비트받아 적기에 길어짐

중요한 건 전체 개수가 아니라 충돌 확률입니다. 코드를 무작위로 뽑으면 생일 문제가 되므로, 발급 수 n 에 대한 충돌 확률은 대략 1 - exp(-n² / 2N) 입니다.

발급 수별 충돌 확률 표 펼치기
발급 수6자리(32^6)에서 충돌 확률
100개0.0005%
1,000개0.047%
10,000개4.6%
46,000개약 63% (생일 한계 근처)

내부 문서 단축링크는 많아야 수천 개입니다. 1,000개에서 0.05% 면 무작위 발급으로도 안전하고, 발급기가 기존 코드와 대조까지 하므로 실제 충돌은 0입니다. 그래서 6자리에서 멈췄습니다.

서버 없이 어떻게 넘기나

이 사이트에는 서버 런타임이 없습니다. 정적 파일만 올라갑니다. 그래서 단축링크마다 리다이렉트 전용 HTML 한 장을 미리 만들어 둡니다. 빌드 때 /s/<코드>/index.html 이 생성되고, 그 안에 세 겹의 이동 수단을 넣습니다.

  1. <meta http-equiv="refresh"> - 자바스크립트가 꺼져 있어도 이동
  2. location.replace() - 즉시 이동하면서 뒤로 가기 기록을 남기지 않음
  3. 본문의 일반 링크 - 둘 다 막힌 환경에서 사용자가 직접 누름

<link rel="canonical"> 로 원본 주소를 가리키고 noindex 를 붙여, 검색 결과에 단축 주소가 아니라 원본이 남게 했습니다.

고려했지만 하지 않은 것

  • 해시 기반 코드: 대상 주소를 해시해 앞 6자리를 쓰면 발급기가 필요 없지만, 같은 문서의 주소가 조금만 바뀌어도 코드가 통째로 바뀝니다. 코드가 변하지 않는 쪽이 공유 용도에 맞아 무작위 + 대조 방식을 택했습니다.
  • 순차 코드: 짧고 충돌이 없지만, 발급 수가 그대로 드러나고 다음 코드를 추측할 수 있습니다.
  • 대소문자 혼용(62자): 4자리로도 1,400만 개가 되지만 구두로 전달할 때 대소문자를 매번 말해야 합니다. 전화로 부르기 어려운 코드는 목적에 어긋납니다.
  • 동적 리다이렉트 서버: 클릭 통계를 얻을 수 있지만, 정적 사이트에 서버를 하나 더 붙여야 하고 그 서버가 죽으면 링크도 죽습니다.

글마다 붙는 공유 버튼

글 위쪽에 그 글의 단축 주소와 복사 버튼을 뒀습니다. 화면에는 경로(/s/<코드>/)만 적고, 복사할 때 현재 도메인을 붙여 전체 주소를 만듭니다. 배포 도메인이 아직 정해지지 않아서, 주소를 콘텐츠에 박아 두면 도메인이 바뀔 때 전부 손대야 하기 때문입니다.

클립보드는 생각보다 잘 막힙니다. 그래서 세 단계로 물러섭니다.

  1. navigator.clipboard.writeText() - 보안 컨텍스트(https·localhost)에서 동작하는 정상 경로
  2. 숨긴 textarea 에 넣고 document.execCommand('copy') - 구형 브라우저와 일부 내장 브라우저 대응
  3. 그것도 막히면 전체 주소를 화면에 띄우고 선택 상태로 만들어 사용자가 바로 복사하게 함

실제로 이 사이트를 iframe 안에서 열어 확인해 보니 1·2단계가 모두 막혔고, 3단계 덕분에 기능이 죽지 않았습니다. 버튼 문구도 상태에 따라 링크 복사복사됨 또는 주소 선택됨 으로 바뀝니다.

남은 함정

단축링크 표는 정적 페이지로 함께 나갑니다. 즉 가리키는 주소가 공개됩니다. 사내망 주소를 넣으면 접속은 못 해도 주소 자체는 노출되므로, 외부에 알려지면 곤란한 주소는 이 표에 넣지 않습니다.

코드를 정규화할 때 입력값의 o0, i·l1, uv 로 되돌리는 처리를 넣어 두면, 사람이 잘못 옮겨 적어도 같은 문서로 도착합니다. 알파벳에서 뺀 글자를 버리지 않고 오타 흡수용으로 쓰는 셈입니다.