개발 이야기3분 읽기

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 + 결합 점입니다.

그리스어는 위치를 본다

ΟΔΟΣ  → οδος
ΣΟΦΟΣ → σοφος

그리스어 시그마는 낱말 끝에서 ς, 그 밖에서 σ입니다. 대문자는 둘 다 Σ입니다. 소문자로 바꿀 때 어느 것을 쓸지는 그 글자가 낱말의 끝인지에 달려 있습니다.

즉 대소문자 변환은 글자 하나만 보고 결정되지 않습니다. 앞뒤를 봐야 하는 경우가 있습니다.

프로그래밍 케이스 변환도 왕복하지 않는다

camelCasesnake_case 변환은 유니코드와 별개로 자체 함정이 있습니다.

XMLHttpRequest   → xml_http_request   → xmlHttpRequest
parseHTMLString  → parse_html_string  → parseHtmlString
IOError          → io_error           → ioError
getURLFor        → get_url_for        → getUrlFor
APIKey           → api_key            → apiKey

연속된 대문자가 어디서 끊기는지 정보가 스네이크로 가면서 사라집니다. 돌아올 때는 그 정보가 없으니 XMLXml이 됩니다.

숫자도 갈립니다.

user_id_2 → userId2

user_id_2가 원래 userId2였는지 userID2였는지 user_id_2 그대로였는지 알 수 없습니다.

API 응답 키를 스네이크에서 카멜로 바꿔 쓰다가 다시 스네이크로 돌려 보내는 코드는 이 지점에서 원래 키와 어긋납니다.

확인 순서

  1. 문자열 비교 목적의 소문자 변환이 로케일을 타지 않는지 봅니다. 터키어 로케일이 시험 값입니다
  2. 대문자 변환 후 길이 제한을 다시 검사합니다. ß·합자에서 길어집니다
  3. 표시용 변환 결과를 저장하지 않습니다. 원본을 두고 표시할 때 변환합니다
  4. 케이스 변환을 왕복시키지 않습니다. 한쪽 표기를 정본으로 두고 경계에서 한 번만 바꿉니다
  5. 연속 대문자가 있는 식별자를 테스트에 넣습니다 — XMLHttpRequest, APIKey, IOError

tools.onuel.dev케이스 변환기는 camelCase·PascalCase·snake_case·kebab-case·CONSTANT_CASE·Title Case 사이를 줄 단위로 바꿉니다. 위의 XMLHttpRequest 같은 값을 넣어 어디서 끊기는지 확인할 수 있습니다. 글자 자체를 다룰 때는 단어·문자 수 세기가 자소 단위로 세고, 두 표기를 대조하려면 텍스트 비교를 씁니다.

  • #유니코드
  • #문자열
  • #국제화