대소문자 변환기

텍스트를 camelCase, snake_case, kebab-case, Title Case 등으로 변환합니다. 로케일에 맞는 대소문자 변환과 줄 도구를 제공합니다.

텍스트

표기 형식

  • UPPERCASE
    XMLHTTPREQUEST HANDLER FOR ISTANBUL CITY GUIDE
  • lowercase
    xmlhttprequest handler for istanbul city guide
  • Title Case
    Xmlhttprequest Handler For Istanbul City Guide
  • Sentence case
    Xmlhttprequest handler for istanbul city guide
  • camelCase
    xmlHttpRequestHandlerForIstanbulCityGuide
  • PascalCase
    XmlHttpRequestHandlerForIstanbulCityGuide
  • snake_case
    xml_http_request_handler_for_istanbul_city_guide
  • kebab-case
    xml-http-request-handler-for-istanbul-city-guide
  • CONSTANT_CASE
    XML_HTTP_REQUEST_HANDLER_FOR_ISTANBUL_CITY_GUIDE
  • dot.case
    xml.http.request.handler.for.istanbul.city.guide

줄 작업

결과
XMLHttpRequest handler for istanbul city guide

줄 수: 1

하나의 단어 분할에서 나오는 열 가지 표기 형식

대소문자를 바꾸는 일은 프로그램이 할 수 있는 가장 단순한 작업처럼 보이지만, 놀라울 만큼 많은 실제 버그의 원인이 되어 왔습니다. 이유 중 하나는 '대소문자'가 사실 하나의 이름을 나눠 쓰는 두 개의 별개 문제라는 점입니다. 텍스트를 UPPERCASE나 lowercase로 바꾸는 것은 Unicode가 정의한 문자 단위 매핑입니다. camelCase나 snake_case로 바꾸는 것은 전혀 다른 일로, 먼저 단어가 어디에 있는지 정한 뒤에야 대소문자를 바꾸고 다시 이어 붙일 수 있습니다. 이 도구는 두 가지를 모두 수행하고 모든 결과를 한 번에 보여 주며, 무엇보다 어떤 답이 언어 설정에 좌우되는지를 알려 줍니다.

프로그래머용 여섯 형식은 모두 같은 단어 분할에서 나오므로 서로 어긋날 수 없습니다. 그 경계는 다음과 같습니다.

  • 소문자 다음에 대문자가 오면 새 단어가 시작되며, 그래서 fooBar가 foo와 Bar가 됩니다.
  • 대문자가 이어지는 구간은 다음 단어가 시작되는 곳, 즉 소문자 바로 앞의 마지막 대문자에서 끝납니다. XMLHttpRequest는 XML, Http, Request가 되고 IOError는 IO와 Error가 됩니다.
  • 숫자 다음에 대문자가 오면 새 단어가 시작되므로 html5Parser는 html5와 Parser가 됩니다.
  • 문자도 숫자도 결합 표시도 아닌 것은 모두 단어를 나눕니다. 공백, 밑줄, 하이픈, 점, 문장 부호가 여기에 해당합니다.

그 밖의 경우 숫자는 앞의 단어에 붙은 채로 남습니다. 그래서 utf8과 address2는 온전히 통과합니다. 이는 lodash와의 의도적인 차이입니다. lodash의 분할기는 숫자가 이어지는 구간을 모두 별개의 단어로 보아 utf8을 utf_8로 만듭니다. 기술적으로는 일관되지만, 변수에 이름을 붙인 사람이 뜻한 바는 아닙니다.

이 분할은 ASCII 문자 클래스가 아니라 Unicode 속성으로 작성되어 영어에만 매이지 않습니다. 키릴 문자 식별자도 라틴 문자와 똑같이 대소문자 경계에서 나뉩니다. 애초에 대소문자 구분이 없는 문자 체계 — 히브리 문자, 아랍 문자, 중국어, 일본어, 한국어 — 에는 찾을 경계가 없으므로 구분 기호로만 나뉘며, 이것은 한계가 아니라 올바른 답입니다.

약어에는 옳은 답이 둘 있습니다

XMLHttpRequest가 XML, Http, Request로 나뉜 뒤 이를 PascalCase로 다시 조합하는 단계에서 정답이 하나뿐이 아닌 물음이 생깁니다. 모든 단어를 다시 조합하면 XmlHttpRequest가 되는데, 이는 lodash가 만들어 내는 결과이자 Google의 Java 스타일 가이드가 요구하는 바입니다. 약어를 평범한 단어로 다루면 ParseXmlDocument 같은 이름에서도 경계가 계속 보이기 때문입니다. 대문자가 이어지는 구간을 그대로 두면 XMLHttpRequest가 되는데, 이는 브라우저 API의 실제 이름이며 DOM 대부분과 ‎.NET의 상당 부분이 따르는 관례입니다.

두 형식 모두 실제 코드베이스에서 쓰이므로 이 도구는 둘 다 제공하며 기본값은 다시 조합하는 쪽입니다. 이 옵션은 camelCase와 PascalCase에만 영향을 줍니다. 다른 형식은 어차피 단어 전체를 다시 조합하므로 XMLHttpRequest는 어느 쪽이든 xml_http_request와 XML_HTTP_REQUEST가 됩니다.

한 가지는 선택이 아니라 고정되어 있습니다. camelCase 이름의 첫 단어는 약어라 하더라도 언제나 전부 소문자가 됩니다. 그대로 두면 PascalCase가 되고, 절반만 두면 어떤 스타일 가이드도 권하지 않으며 모든 독자가 걸려 넘어지는 xMLHttpRequest가 되기 때문입니다. 그래서 XMLHttpRequest는 어느 설정에서든 xmlHttpRequest가 되고, parseXMLDocument는 요청하면 가운데의 약어를 그대로 유지합니다.

터키어의 i, 그리고 로케일이 선택인 이유

JavaScript에는 대문자로 바꾸는 메서드가 둘 있고, 그 차이는 운영 중인 시스템을 무너뜨려 왔습니다. toUpperCase는 로케일과 무관한 Unicode 매핑을 적용하여 문자 i를 I로 만듭니다. toLocaleUpperCase는 특정 언어의 규칙을 적용하는데, 터키어와 아제르바이잔어에서 i의 대문자는 점이 있는 İ입니다. 그 알파벳에는 점이 없는 ı도 있고 그 대문자가 I이기 때문입니다. 터키어 사용자의 입력을 그 사용자의 로케일로 대문자화한 뒤 프로토콜 키워드와 비교하면, 거의 똑같아 보이는 문자 하나 때문에 비교가 실패합니다.

이것은 문자열 처리에서 가장 흔한 국제화 버그이며, 내 일이 되기 전까지는 보이지 않습니다. 그래서 이 도구는 결코 추측하지 않습니다. 기본값은 로케일과 무관한 매핑으로, 식별자와 프로토콜 토큰, HTTP 헤더, 데이터베이스 키에 필요한 값입니다. 언어를 고르면 그 언어의 규칙으로 바뀌며, 그 선택이 결과를 바꿀 때마다 로케일과 무관한 답을 나란히 보여 줍니다. 그 언어가 무엇을 했는지 그대로 보이도록 하기 위해서입니다.

'i'.toUpperCase()             // 'I'
'i'.toLocaleUpperCase('tr')   // 'İ'  dotted capital
'I'.toLocaleLowerCase('tr')   // 'ı'  dotless lowercase
'ISTANBUL'.toLocaleLowerCase('tr')  // 'ıstanbul'

다른 세 언어도 답을 바꾸며 모두 제공됩니다. 리투아니아어는 다른 악센트가 붙은 대문자를 소문자로 바꿀 때 i 위의 점을 남기므로 Ì는 결합 점과 결합 그레이브가 뒤따르는 i가 되고, 미리 조합된 단일 문자 ì가 되지 않습니다. 그리스어는 대문자로 바꿀 때 토노스를 없애므로 άνθρωπος는 ΆΝΘΡΩΠΟΣ가 아니라 ΑΝΘΡΩΠΟΣ가 됩니다. 그리스어에서는 대문자에 악센트를 적지 않기 때문입니다. 아제르바이잔어는 터키어와 점이 있는 i와 없는 i를 공유합니다.

유명한 두 사례는 로케일 규칙이 아니며, 어느 쪽이 어느 쪽인지 알아 둘 만합니다. 독일어의 ß는 모든 로케일에서 SS가 되는데, 이는 독일어 규칙이 아니라 Unicode의 SpecialCasing 규칙이기 때문입니다. 그래서 이 매핑은 되돌릴 수도 없어 SS는 ss로 돌아갑니다. 또 그리스어 시그마는 낱말 끝에서 σ가 아니라 종형 ς가 되는데 이것도 어디서나 같습니다. 언어 설정이 아니라 문자 위치에 대한 문맥 조건이기 때문입니다. 둘 다 로케일을 전혀 고르지 않아도 이 도구에서 확인할 수 있습니다.

여기서 Title case와 Sentence case는 기계적입니다

이 도구의 Title Case는 각 단어의 첫 글자를 대문자로 만들고 나머지를 소문자로 만듭니다. Sentence case는 텍스트의 첫 글자와 그 뒤 각 문장의 첫 글자를 대문자로 만들며, 문장은 마침표나 느낌표, 물음표, 줄임표 뒤에 공백이 오는 곳에서 끝납니다. 둘 다 입력을 먼저 소문자로 바꾸는데, 바로 그 덕분에 고함치듯 들어온 텍스트에도 쓸모가 있습니다.

이는 의도적으로 기계적인 규칙이며, 대안을 설명해 둘 필요가 있습니다. 영어 스타일 가이드들은 제목 표기를 어떤 짧은 낱말을 소문자로 남길지로 정의하는데, 그 목록에서도, 콜론 뒤 낱말을 대문자로 할지에서도, 네 글자 이상의 전치사에서도 서로 어긋납니다. 그중 하나를 여기에 적용한다면 히브리어나 일본어, 터키어로 쓰는 독자에게 개념 자체가 옮겨지지 않는 미국 신문의 관례를 보내는 셈입니다. 모든 낱말을 대문자로 만드는 편이 예측 가능하고 어떤 언어로도 설명할 수 있으며 나중에 손으로 고치기도 쉽습니다.

Sentence case의 공백 조건은 3.5 metres가 3.5 Metres가 되지 않게 막아 줍니다. 그곳의 마침표 뒤에는 숫자가 오기 때문입니다. 다만 약어 문제는 해결하지 못합니다. e.g. this는 여전히 가짜 문장을 시작합니다. 'e.g.'가 문장의 끝이 아님을 알려면 해당 언어의 약어 목록이 필요하기 때문입니다. 단어 안의 아포스트로피도 새 단어를 시작하지 않으므로 it's는 한 번만 대문자가 되어 It'S가 아니라 It's가 됩니다.

정렬: 알파벳 순서인가 바이트 순서인가

줄 도구는 공백을 제거하고 중복을 지우고 정렬하고 뒤집고 번호를 매기는데, 정렬은 로케일 문제를 두 번째로 불러옵니다. 알파벳 순서는 Unicode의 성질이 아니라 언어의 성질입니다. 독일어는 ä를 a 옆에 두므로 a, ä, z 순입니다. 스웨덴어는 이를 별개의 글자로 보아 알파벳 끝에 두므로 a, z, ä 순입니다. 둘 다 자기 나라에서는 옳으며, 도구는 페이지 위에서 고른 로케일을 사용합니다.

다른 선택지는 코드 포인트 순서로, 답이 다른 무언가와 일치해야 할 때 필요합니다. 코드 포인트 비교는 UTF-8의 바이트 순서이므로 LC_ALL=C sort와 git, 그리고 대부분 프로그래밍 언어의 기본 문자열 비교가 내놓는 결과를 그대로 재현합니다. 즉 모든 대문자가 모든 소문자보다 앞에 오므로 B가 a보다 먼저입니다. 도구의 출력과 맞춰 볼 때는 이쪽을, 사람이 읽을 목록을 만들 때는 언어별 정렬을 고르십시오.

자연스러운 숫자 순서 — item2가 item10보다 뒤가 아니라 앞에 오는 것 — 는 언어별 정렬의 기능이므로 알파벳 규칙에서만 쓸 수 있습니다. 바이트 순서에는 문자열 안의 숫자라는 개념이 없고, 그것이 흉내 내는 정렬에도 없습니다. 둘을 함께 제공하면 출력이 지킬 수 없는 약속을 하는 셈이 됩니다.

작업은 정해진 순서로 실행되며 그 순서에는 의미가 있습니다. 공백 제거가 중복 제거보다 먼저이므로 끝의 공백만 달랐던 두 줄이 같은 줄로 인식됩니다. 정렬이 역순보다 먼저이므로 역순은 언제나 '지금 보고 있는 것을 뒤집는다'는 뜻입니다. 빈 줄의 수와 제거된 중복의 수는 모두 보고됩니다. 조용한 집계야말로 목록이 생각과 달랐다는 사실을 놓치게 만드는 원인이기 때문입니다.

어디에서 실행되나요

모든 처리는 브라우저 안에서 이루어집니다. 대소문자 매핑은 JavaScript 엔진 자체의 Unicode 구현에서, 정렬 순서는 Intl.Collator에서 옵니다. 따라서 결과는 누군가 몇 년 전에 베껴 둔 표가 아니라 실제로 여러분의 문자열을 처리하게 될 실행 환경과 일치합니다. 붙여 넣은 내용이 업로드되거나 저장되거나 기록되지 않으므로, 이름이나 로그 줄처럼 대소문자만 바꾸려고 서버에 보내고 싶지 않은 것에도 안심하고 쓸 수 있습니다.

자주 묻는 질문

입력한 텍스트가 서버로 전송되나요?
아닙니다. 모든 변환은 JavaScript 엔진 자체의 Unicode 대소문자 매핑을 사용해 브라우저 안에서 실행되며, 붙여 넣은 내용은 업로드되거나 기록되지 않습니다.
터키어를 고르면 왜 결과가 두 개 나오나요?
터키어가 결과를 바꾸기 때문입니다. 터키어 알파벳에는 점이 있는 i와 점이 없는 ı가 모두 있어서 i의 대문자가 I가 아니라 İ입니다. 로케일이 결과를 바꿀 때마다 도구는 로케일과 무관한 답을 나란히 보여 줍니다. 식별자와 프로토콜 토큰에는 그쪽이 필요하기 때문입니다.
로케일과 무관한 매핑을 써야 하나요, 언어별 매핑을 써야 하나요?
기계가 읽는 것 — 식별자, HTTP 헤더, 데이터베이스 키, 프로토콜 토큰, 파일 이름 — 에는 로케일과 무관한 매핑을 쓰십시오. 사람이 그 언어로 읽는 텍스트에는 언어별 매핑을 쓰십시오. 이 둘을 섞는 것이 고전적인 터키어 i 버그입니다.
XMLHttpRequest가 왜 XmlHttpRequest로 바뀌나요?
기본값이 약어를 다시 조합하는 것이기 때문이며, 이는 lodash와 여러 주요 스타일 가이드가 하는 방식입니다. 약어 옵션을 켜면 대문자 구간이 유지되어 XMLHttpRequest가 그대로 돌아옵니다. 이 옵션은 camelCase와 PascalCase에만 영향을 줍니다. 다른 형식은 어차피 모든 단어를 다시 조합하기 때문입니다.
lodash는 나누는 utf8이 여기서는 왜 한 단어로 남나요?
여기서는 숫자가 앞 단어에 붙기 때문에 utf8과 address2가 온전히 통과합니다. 숫자 다음에 대문자가 오는 경우에는 새 단어가 시작되며, 그것이 html5Parser를 html5와 Parser로 나눕니다.
Title Case가 of나 the 같은 짧은 낱말까지 대문자로 만드는 이유는 무엇인가요?
규칙이 의도적으로 기계적이기 때문입니다. 영어 스타일 가이드들은 어떤 짧은 낱말을 소문자로 남길지에서 서로 어긋나며, 그 관례는 이 사이트가 다루는 다른 언어로 옮겨지지 않습니다. 모든 낱말을 대문자로 만드는 편이 예측 가능하고 손으로 고치기도 쉽습니다.
코드 포인트 정렬에서는 왜 자연스러운 숫자 순서를 쓸 수 없나요?
자연스러운 순서는 언어별 정렬의 기능입니다. 코드 포인트 순서는 바이트 순서라서 문자열 안의 숫자라는 개념이 없고, 그것이 재현하는 LC_ALL=C sort에도 없습니다. 둘을 함께 제공하면 흉내 내던 도구와 더 이상 일치하지 않는 출력이 나옵니다.
로케일을 고르지 않아도 ß가 SS가 되는 이유는 무엇인가요?
그 매핑이 독일어 규칙이 아니라 Unicode의 SpecialCasing 규칙이어서 어디에나 적용되기 때문입니다. 되돌릴 수도 없어서 SS는 ß가 아니라 ss가 됩니다. 그리스어의 종형 시그마도 같은 방식으로, 로케일이 아니라 글자의 위치에 따라 결정됩니다.