toLowerCase가 언어를 모르면 생기는 일
터키어에서 I는 i가 아닙니다. 대소문자 변환이 왕복하지 않고 길이가 변하는 경우를 실행 결과로 정리했습니다.
아이디를 비교하기 전에 소문자로 맞춥니다.
if (input.toLowerCase() === stored.toLowerCase()) { /* 같은 계정 */ }
영어만 다루면 문제가 없습니다. 그 밖에서는 몇 가지가 어긋납니다.
터키어의 I는 i가 아니다
'I'.toLowerCase() // "i"
'I'.toLocaleLowerCase('tr') // "ı" 점 없는 ı
'i'.toUpperCase() // "I"
'i'.toLocaleUpperCase('tr') // "İ" 점 있는 İ
터키어에는 점 있는 i와 점 없는 ı가 별개 글자로 있고, 각각의 대문자가 İ와 I입니다. 그래서 같은 I가 언어에 따라 i가 되기도 ı가 되기도 합니다.
브라우저나 서버의 로케일이 터키어로 설정돼 있으면 toLowerCase()가 로케일을 따라갈 수 있습니다. "ID".toLowerCase()가 "ıd"가 되면 "id"와 비교가 실패합니다. 실제로 이 때문에 터키 사용자만 로그인이 안 되는 버그가 반복해서 보고돼 왔고, 영어권 개발자가 재현하기 어려운 종류입니다.
대소문자를 비교 목적으로 맞출 때는 로케일에 의존하지 않는 경로를 씁니다.
input.toLowerCase() === stored.toLowerCase() // 로케일 영향을 받을 수 있다
input.localeCompare(stored, undefined, { sensitivity: 'accent' }) === 0
길이가 변한다
"ß" (1자) → 대문자 "SS" (2자)
"fi" (1자) → 대문자 "FI" (2자)
"ʼn" (1자) → 대문자 "ʼN" (2자)
독일어 ß의 대문자는 SS입니다. 합자 fi는 FI가 됩니다. 대문자로 바꾸면 문자열이 길어집니다.
입력란에 20자 제한을 걸어 두고 저장 전에 대문자로 바꾸면, 20자가 21자가 되어 데이터베이스에서 잘릴 수 있습니다.
왕복하지 않는다
"ß" → 대문자 → 소문자 = "ss" 원래대로 안 돌아옴
"İ" → 대문자 → 소문자 = "i̇" 2자가 됨
"ff" → 대문자 → 소문자 = "ff" 원래대로 안 돌아옴
대문자로 바꿨다가 다시 소문자로 바꾸면 출발점이 아닙니다. 화면 표시용으로 대문자 변환을 걸어 두고 그 값을 저장하면 원본이 사라집니다.
İ의 소문자가 두 글자가 되는 것도 같은 종류입니다 — i + 결합 점입니다.
그리스어는 위치를 본다
ΟΔΟΣ → οδος
ΣΟΦΟΣ → σοφος
그리스어 시그마는 낱말 끝에서 ς, 그 밖에서 σ입니다. 대문자는 둘 다 Σ입니다. 소문자로 바꿀 때 어느 것을 쓸지는 그 글자가 낱말의 끝인지에 달려 있습니다.
즉 대소문자 변환은 글자 하나만 보고 결정되지 않습니다. 앞뒤를 봐야 하는 경우가 있습니다.
프로그래밍 케이스 변환도 왕복하지 않는다
camelCase ↔ snake_case 변환은 유니코드와 별개로 자체 함정이 있습니다.
XMLHttpRequest → xml_http_request → xmlHttpRequest
parseHTMLString → parse_html_string → parseHtmlString
IOError → io_error → ioError
getURLFor → get_url_for → getUrlFor
APIKey → api_key → apiKey
연속된 대문자가 어디서 끊기는지 정보가 스네이크로 가면서 사라집니다. 돌아올 때는 그 정보가 없으니 XML이 Xml이 됩니다.
숫자도 갈립니다.
user_id_2 → userId2
user_id_2가 원래 userId2였는지 userID2였는지 user_id_2 그대로였는지 알 수 없습니다.
API 응답 키를 스네이크에서 카멜로 바꿔 쓰다가 다시 스네이크로 돌려 보내는 코드는 이 지점에서 원래 키와 어긋납니다.
확인 순서
- 문자열 비교 목적의 소문자 변환이 로케일을 타지 않는지 봅니다. 터키어 로케일이 시험 값입니다
- 대문자 변환 후 길이 제한을 다시 검사합니다. ß·합자에서 길어집니다
- 표시용 변환 결과를 저장하지 않습니다. 원본을 두고 표시할 때 변환합니다
- 케이스 변환을 왕복시키지 않습니다. 한쪽 표기를 정본으로 두고 경계에서 한 번만 바꿉니다
- 연속 대문자가 있는 식별자를 테스트에 넣습니다 —
XMLHttpRequest,APIKey,IOError
tools.onuel.dev의 케이스 변환기는 camelCase·PascalCase·snake_case·kebab-case·CONSTANT_CASE·Title Case 사이를 줄 단위로 바꿉니다. 위의 XMLHttpRequest 같은 값을 넣어 어디서 끊기는지 확인할 수 있습니다. 글자 자체를 다룰 때는 단어·문자 수 세기가 자소 단위로 세고, 두 표기를 대조하려면 텍스트 비교를 씁니다.
- #유니코드
- #문자열
- #국제화