버그 추적
짝 없는 서로게이트가 URL 직렬화를 깨뜨렸다
11,172자 완성형 한글 퍼징 테스트 89개 중 1개가 실패했다. encodeURIComponent에 lone surrogate를 넣으면 URIError가 난다.
89개 중 1개가 실패했다
타자속도 측정기의 코어에는 테스트가 89개 있습니다. 그중에는 완성형 한글 11,172자를 전수로 넣어 보는 퍼징 테스트도 있습니다. 한글 타수 계산이 어떤 글자에서도 깨지지 않는지 확인하는 용도입니다.
그 89개 중 하나가 실패했습니다. 테스트 이름은 이랬습니다."무작위 퍼징: 짝 없는 서로게이트도 예외를 던지지 않는다"
짝 없는 서로게이트가 뭔가
자바스크립트 문자열은 UTF-16입니다. 기본 다국어 평면을 벗어나는 문자 (이모지 같은 것)는 두 개의 코드 유닛이 짝을 이뤄 하나의 문자를 만듭니다. 이 짝을 서로게이트 페어라고 합니다.
문제는 짝이 깨진 상태의 문자열을 만들 수 있다는 점입니다. 이모지를 문자열 중간에서 자르거나, \uD800 같은 값을 직접 넣으면 앞짝만 있고 뒷짝이 없는 문자열이 됩니다. 화면에는 깨진 네모로 보입니다.
어디서 터졌나
이 도구는 측정 결과를 URL에 담아 공유할 수 있습니다. 상태를 직렬화하면서 encodeURIComponent를 쓰는데, 여기가 터지는 지점이었습니다.
encodeURIComponent('\uD800')
// URIError: URI malformedencodeURIComponent는 짝 없는 서로게이트를 만나면 예외를 던집니다. 반환값이 이상한 게 아니라 던집니다. 감싸지 않으면 그 자리에서 함수가 중단됩니다.
실제 사용자가 이걸 만들 일이 있을까요? 있습니다. 이모지가 든 텍스트를 복사하다 잘리거나, 다른 앱에서 넘어온 문자열이 깨져 있거나. 흔하지는 않지만 일어나면 공유 기능이 통째로 죽습니다.
고친 방법
정규식으로 짝 없는 서로게이트를 찾아 대체 문자 U+FFFD로 바꾸는 헬퍼를 만들고, 인코딩을 그 헬퍼를 거치는 래퍼로 감쌌습니다. 깨진 문자는 화면에 깨진 대로 보이지만 예외는 나지 않고 공유 링크는 만들어집니다.
수정 후 89개 전수 테스트와 11,172자 완성형 한글 퍼징이 모두 통과했습니다.
퍼징 테스트를 왜 쓰나
이 버그는 사람이 예제를 골라 쓰는 방식의 테스트로는 거의 나오지 않습니다. "짝 없는 서로게이트를 넣어 보자"는 생각을 하려면 이미 그 문제를 알고 있어야 하니까요.
퍼징은 반대 방향입니다. 정의역 전체를 기계적으로 훑습니다.11,172자를 전부 넣고, 무작위 코드 포인트도 섞어 넣습니다. 그중 하나가 걸리면 그게 우리가 몰랐던 구멍입니다.
코어가 순수 함수라 이게 가능했습니다. DOM도 브라우저도 없이 Node에서 수만 번 호출할 수 있으니 전수 검사의 비용이 거의 없습니다. 로직을 순수 모듈로 밀어낼수록 이런 검증의 범위가 넓어집니다.