사내 문서 주소는 길고 잘 바뀝니다. 회의 중에 주소를 부르거나 화이트보드에 적어야 할 때가 있어서, 짧은 코드로 대신 부를 수단이 필요했습니다. 이 사이트에 /s/<코드>/ 형태의 단축링크를 붙이면서 정한 것들을 적어 둡니다.
알파벳에서 무엇을 뺐나
사람이 옮겨 적는 순간 0과 o, 1과 l 은 같은 글자가 됩니다. 둘 다 빼면 알파벳이 크게 줄고, 둘 다 남기면 오타가 납니다. 그래서 짝에서 한쪽만 남겼습니다. 숫자를 남기고 글자를 뺐습니다. 손으로 쓴 글자가 숫자보다 더 자주 오인되기 때문입니다.
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,576 | 20비트 | 100만 개. 내부용으로도 빠듯 |
| 5자리 | 33,554,432 | 25비트 | 3,300만 개. 충분하지만 여유가 적음 |
| 6자리 | 1,073,741,824 | 30비트 | 10.7억 개. 채택 |
| 7자리 | 34,359,738,368 | 35비트 | 받아 적기에 길어짐 |
중요한 건 전체 개수가 아니라 충돌 확률입니다. 코드를 무작위로 뽑으면 생일 문제가 되므로, 발급 수 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 이 생성되고, 그 안에 세 겹의 이동 수단을 넣습니다.
<meta http-equiv="refresh">- 자바스크립트가 꺼져 있어도 이동location.replace()- 즉시 이동하면서 뒤로 가기 기록을 남기지 않음- 본문의 일반 링크 - 둘 다 막힌 환경에서 사용자가 직접 누름
<link rel="canonical"> 로 원본 주소를 가리키고 noindex 를 붙여, 검색 결과에 단축 주소가 아니라 원본이 남게 했습니다.
고려했지만 하지 않은 것
- 해시 기반 코드: 대상 주소를 해시해 앞 6자리를 쓰면 발급기가 필요 없지만, 같은 문서의 주소가 조금만 바뀌어도 코드가 통째로 바뀝니다. 코드가 변하지 않는 쪽이 공유 용도에 맞아 무작위 + 대조 방식을 택했습니다.
- 순차 코드: 짧고 충돌이 없지만, 발급 수가 그대로 드러나고 다음 코드를 추측할 수 있습니다.
- 대소문자 혼용(62자): 4자리로도 1,400만 개가 되지만 구두로 전달할 때 대소문자를 매번 말해야 합니다. 전화로 부르기 어려운 코드는 목적에 어긋납니다.
- 동적 리다이렉트 서버: 클릭 통계를 얻을 수 있지만, 정적 사이트에 서버를 하나 더 붙여야 하고 그 서버가 죽으면 링크도 죽습니다.
글마다 붙는 공유 버튼
글 위쪽에 그 글의 단축 주소와 복사 버튼을 뒀습니다. 화면에는 경로(/s/<코드>/)만 적고, 복사할 때 현재 도메인을 붙여 전체 주소를 만듭니다. 배포 도메인이 아직 정해지지 않아서, 주소를 콘텐츠에 박아 두면 도메인이 바뀔 때 전부 손대야 하기 때문입니다.
클립보드는 생각보다 잘 막힙니다. 그래서 세 단계로 물러섭니다.
navigator.clipboard.writeText()- 보안 컨텍스트(https·localhost)에서 동작하는 정상 경로- 숨긴 textarea 에 넣고
document.execCommand('copy')- 구형 브라우저와 일부 내장 브라우저 대응 - 그것도 막히면 전체 주소를 화면에 띄우고 선택 상태로 만들어 사용자가 바로 복사하게 함
실제로 이 사이트를 iframe 안에서 열어 확인해 보니 1·2단계가 모두 막혔고, 3단계 덕분에 기능이 죽지 않았습니다. 버튼 문구도 상태에 따라 링크 복사 → 복사됨 또는 주소 선택됨 으로 바뀝니다.
남은 함정
단축링크 표는 정적 페이지로 함께 나갑니다. 즉 가리키는 주소가 공개됩니다. 사내망 주소를 넣으면 접속은 못 해도 주소 자체는 노출되므로, 외부에 알려지면 곤란한 주소는 이 표에 넣지 않습니다.
코드를 정규화할 때 입력값의 o→0, i·l→1, u→v 로 되돌리는 처리를 넣어 두면, 사람이 잘못 옮겨 적어도 같은 문서로 도착합니다. 알파벳에서 뺀 글자를 버리지 않고 오타 흡수용으로 쓰는 셈입니다.