대소문자 변환기

다양한 텍스트 케이스 스타일 변환

대문자
-
소문자
-
제목 케이스
-
문장 케이스
-
카멜 케이스
-
파스칼 케이스
-
스네이크 케이스
-
케밥 케이스
-
상수 케이스
-

Unicode 대소문자 매핑은 단순한 A↔Z 치환이 아니다

ASCII는 대소문자를 완벽한 양방향 매핑처럼 보이게 만들었습니다. A–Z와 a–z가 정확히 짝을 이루니 대문자화·소문자화가 가역처럼 느껴집니다. Unicode는 이 모델을 의도적으로 깨뜨립니다. 표준의 SpecialCasing.txt는 1:다(one-to-many), 로케일 의존, 문맥 의존 매핑을 정의하며, JavaScript의 toUpperCase()와 toLowerCase()는 이를 충실히 구현합니다 — 그래서 이 놀라움은 프로덕션 코드에서 터집니다. 독일어 에스체트(ß)가 대표적인 1:다 케이스입니다. 'ß'.toUpperCase()는 'SS'를 반환하므로 출력이 입력보다 길어집니다. 대문자 ẞ(U+1E9E)가 2008년 Unicode에 추가되었고 2017년부터 독일어 정서법에서도 공식 허용되었지만, 기본 매핑은 여전히 SS를 내놓습니다. 그 결과 'straße'.toUpperCase().toLowerCase()는 'strasse'로 돌아옵니다 — 왕복 변환이 원래 철자를 파괴합니다. 터키어와 아제르바이잔어는 점 있는 i와 점 없는 ı를 구분합니다. 터키어 로케일에서 'i'.toLocaleUpperCase('tr')는 'İ'(U+0130)를, 'I'.toLocaleLowerCase('tr')는 'ı'(U+0131)를 반환합니다. 한편 기본 로케일에서 'İ'.toLowerCase()는 'i' 뒤에 결합 점(U+0307)이 붙은 문자열을 반환해 .length가 2가 됩니다. 이것이 그 유명한 Turkish-I 버그 계열입니다 — 설정 키나 프로토콜 토큰을 소문자화하는 소프트웨어는 기본 로케일이 터키어인 머신에서 도는 순간 깨집니다. 그리스어는 문맥 의존성을 더합니다. 대문자 Σ는 단어 중간에서는 σ로, 단어 끝에서는 ς로 소문자화됩니다(Unicode의 Final_Sigma 규칙). JavaScript도 이 규칙을 적용합니다. 여기서 나오는 실무 규칙: lower(upper(x)) === x를 가정하지 말 것, 케이스 변환 전후로 길이가 보존된다고 가정하지 말 것, 보안이 걸린 정규화 단계에 단순 케이스 변환을 쓰지 말 것.
'ß'.toUpperCase()             // 'SS' — one char in, two out
'straße'.toUpperCase()
  .toLowerCase()              // 'strasse' — ß never comes back

'i'.toLocaleUpperCase('tr')   // 'İ' (U+0130, dotted capital I)
'I'.toLocaleLowerCase('tr')   // 'ı' (U+0131, dotless i)
'İ'.toLowerCase().length      // 2 — 'i' + combining dot (U+0307)

'ΣΟΦΟΣ'.toLowerCase()         // 'σοφος' — medial σ, final ς

단어는 어디서 끝나는가 — 경계 탐지와 약어 규칙

모든 케이스 변환기는 다른 무엇보다 먼저 한 가지 질문에 답해야 합니다: 단어가 무엇인가? 언더스코어, 하이픈, 공백으로 나누는 건 쉽습니다. 어려운 건 대문자가 연속되는 camelCase 입력입니다. 대문자마다 새 단어를 시작하는 순진한 규칙은 parseHTMLDocument를 parse / H / T / M / L / Document로 찢어버립니다. 제대로 된 토크나이저는 두 가지 규칙을 씁니다: 소문자→대문자 전환에서 새 단어를 열고, 대문자 연속 구간 안에서는 바로 뒤에 소문자가 오는 마지막 대문자에서 새 단어를 엽니다 — 그래서 parse / HTML / Document가 나옵니다. 숫자는 본질적으로 모호합니다. v2Response는 v2 / Response일까요, v / 2 / Response일까요? base64Encode는 base64 / Encode일까요, base / 64 / Encode일까요? 보편적 정답은 없습니다. 대부분의 변환기는 숫자를 앞 단어에 붙이며, 바로 그래서 user_id_2와 user_id2가 같은 camelCase 출력으로 합쳐지곤 합니다. 기계적 규칙이 서로 충돌하기 때문에 스타일 가이드들은 약어에 대해 명시적 입장을 정했습니다. Google Java Style Guide는 약어를 일반 단어로 취급합니다: XMLHTTPRequest가 아니라 XmlHttpRequest, newCustomerID가 아니라 newCustomerId. Swift API Design Guidelines는 위치에 따라 약어를 균일하게 대문자 또는 소문자로 쓰라고 요구합니다 — 이름 맨 앞이면 utf8Bytes, 중간이면 userSMTPServer. Go는 반대로 갔습니다: golint는 정본 표기를 유지해야 하는 initialism 목록(ID, URL, HTTP, API, JSON 등 약 40개)을 하드코딩했고, 그래서 관용적인 Go 코드는 userID와 ServeHTTP로 씁니다. 결국 같은 문구도 생태계마다 '올바른' camelCase가 다릅니다. 변환기는 하나의 정책을 골라 일관되게 적용할 수 있을 뿐입니다.

케이스 변환은 손실 변환이다 — 왕복해도 원본이 돌아오지 않는다

컨벤션 간 변환은 정보를 파괴합니다. 정확히 어떤 정보가 사라지는지 알아둘 가치가 있습니다. 대부분의 변환기는 입력을 정규 단어 목록(canonical word list) — 소문자 단어들, 구분자는 버림 — 으로 정규화한 뒤 대상 스타일로 다시 조립합니다. 같은 단어 목록을 만들어내는 두 입력은 그 순간부터 영구히 구별 불가능해집니다. 첫 번째 희생자는 단어 내부의 대문자입니다. HTML_parser는 camelCase에서 htmlParser가 되고, 되돌리면 html_parser가 나옵니다 — HTML이 원래 대문자였다는 기록은 어디에도 없습니다. 두 번째는 숫자 경계입니다. user_id_2는 userId2가 되고, 역변환은 user_id2를 내놓습니다. 숫자에는 구분자가 어디 있었는지 표시할 대소문자 정보가 없기 때문입니다. 세 번째는 선행 구분자입니다. Python에서 내부 멤버를 뜻하는 관례인 _private는 camelCase에서 private가 됩니다 — 의미를 담은 접두사가 그냥 사라지고, Python의 name mangling을 유발하는 이중 언더스코어도 마찬가지입니다. 연속 구분자(a__b)는 하나로 붕괴합니다. 엔지니어링 결론: 이름을 재유도하지 마세요. 계층 간 식별자를 매핑하는 코드 생성기 — ORM 컬럼↔필드, protobuf 메시지↔JSON — 는 왕복 변환을 믿는 대신 원본 이름을 파생 이름과 함께 저장합니다. Protocol Buffers가 정확히 그렇게 합니다: 모든 필드는 명시적인 json_name을 갖고 있어, 손실 변환이 API 일부를 조용히 개명하는 일이 없습니다.
user_id_2   → camelCase  → userId2
userId2     → snake_case → user_id2      // '_2' boundary lost

HTML_parser → camelCase  → htmlParser
htmlParser  → snake_case → html_parser   // HTML capitals lost

_private    → camelCase  → private       // leading '_' lost
a__b        → camelCase  → aB            // double separator collapsed

취향이 아니라 스펙이 케이스를 강제하는 영역들

명명 규칙은 보통 사회적 계약이지만, 몇몇 시스템은 케이스를 강제 규칙으로 만듭니다. HTTP 헤더 필드 이름은 RFC 9110에 따라 대소문자를 구분하지 않으며, HTTP/2와 HTTP/3는 한발 더 나아가 와이어 상에서 필드 이름이 소문자일 것을 요구합니다 — 현대 프록시가 Content-Type이 아니라 content-type을 보여주는 이유입니다. 환경 변수는 POSIX 시스템에서 대소문자를 구분하고(PATH와 path가 공존 가능) 대문자가 관례입니다. 반면 Windows에서는 대소문자를 구분하지 않아 크로스 플랫폼 스크립트의 상호운용 함정이 됩니다. CSS 커스텀 프로퍼티는 대소문자를 구분합니다(--Main-Color와 --main-color는 다른 변수). HTML 속성명과 태그명은 구분하지 않는데도요. 커스텀 엘리먼트 이름은 소문자여야 하고 하이픈을 포함해야 합니다(my-widget) — 스타일이 아니라 HTML 스펙입니다. npm은 2017년부터 새 패키지 이름에 대문자를 거부합니다. JSONStream 같은 기존 혼합 케이스 패키지는 기득권으로 남아 있습니다. 데이터베이스는 따옴표 없는 식별자를 정반대 방향으로 접습니다(folding): PostgreSQL은 소문자로, Oracle은 대문자로 — 둘 다 SQL 표준을 근거로 듭니다. Postgres에서 따옴표를 써서 "UserAccounts"로 테이블을 만들면 이후 모든 쿼리가 영원히 따옴표를 붙여야 하고, 이 스키마를 Oracle로 옮기면 접힘 방향이 뒤집힙니다. 마지막 함정은 파일시스템입니다. macOS의 APFS와 Windows의 NTFS는 기본적으로 대소문자 비구분(단, 보존은 함)이고, Linux ext4는 구분합니다. button.tsx로 해석되는 import './Button'은 모든 개발자 노트북에서 동작하다가 Linux CI에서만 실패합니다 — "works on my machine"의 가장 확실한 공급원 중 하나입니다.

컨벤션을 가로지르는 리팩터링 실전 워크플로

도메인 개념 하나를 개명한다는 건 모든 컨벤션을 동시에 추적한다는 뜻입니다: TypeScript의 userAccount, SQL과 Python의 user_account, CSS 클래스와 URL의 user-account, 환경 변수의 USER_ACCOUNT, 클래스명의 UserAccount. 한 가지 변형만 검색하면 사용처의 일부만 찾게 됩니다. 신뢰할 수 있는 워크플로는 먼저 모든 변형을 생성하고(이 도구가 붙여넣기 한 번으로 해줍니다), alternation으로 한꺼번에 검색한 뒤 — ripgrep이면 한 번의 패스로 끝납니다 — 무엇이든 치환하기 전에 결과를 검토하는 것입니다. 치환 자체는 IDE 지원으로 안전해집니다. JetBrains의 "Preserve case" 치환 옵션과 VS Code의 preserve-case 토글(치환 상자의 AB 버튼)은 userAccount→memberProfile과 UserAccount→MemberProfile을 한 번의 작업으로 처리하므로, 다섯 번 따로 치환할 필요가 없습니다. API 경계에서는 아예 손으로 변환하지 마세요. 와이어 포맷은 흔히 snake_case이고(Stripe, 대부분의 Python/Ruby 백엔드) JS 클라이언트는 camelCase를 기대합니다. 직렬화 계층에서 선언된 정책으로 한 번만 변환하세요 — Rust에서는 serde의 rename_all = "camelCase", Java에서는 Jackson의 PropertyNamingStrategies.SNAKE_CASE, Node에서는 camelcase-keys. 한 가지 주의: 깊은 키 변환은 키가 식별자가 아니라 데이터인 맵을 건너뛰어야 합니다 — 외부 ID로 키가 잡힌 딕셔너리를 케이스 변환하면 데이터가 조용히 오염됩니다.
# Find every variant of "user account" in one ripgrep pass
rg -n '(userAccount|UserAccount|user_account|user-account|USER_ACCOUNT)'

# Rust: convert once at the serialization boundary
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UserAccount {
    user_id: u64,          // wire: "userId"
    display_name: String,  // wire: "displayName"
}
최종 수정:

도구 소개

대소문자 변환기는 텍스트를 camelCase, PascalCase, snake_case, kebab-case, CONSTANT_CASE 등 다양한 프로그래밍 명명 규칙으로 변환합니다. 언어와 프레임워크마다 권장 규칙이 다릅니다 — JavaScript는 camelCase, Python은 snake_case, CSS 클래스는 kebab-case, 환경 변수는 CONSTANT_CASE를 사용합니다. 수작업으로 바꾸면 실수가 잦아 도구로 변환하는 것이 안전합니다.

사용 방법

  1. 변환하고 싶은 텍스트를 입력란에 입력하거나 붙여넣습니다.
  2. 결과 그리드에서 모든 변환 결과가 동시에 표시됩니다.
  3. 필요한 표기(camelCase, kebab-case 등) 카드를 찾습니다.
  4. 해당 카드의 Copy 버튼을 눌러 클립보드에 복사합니다.
  5. IDE, 터미널, 설정 파일, 커밋 메시지에 붙여넣어 사용합니다.

주요 사용 사례

  • 데이터베이스 컬럼(snake_case)을 TypeScript 필드(camelCase)로 변환
  • 디자인 토큰명에서 CSS 클래스명(kebab-case) 생성
  • API 연동 시 다른 언어 규약에 맞게 JSON 키 변환
  • 사람이 읽기 쉬운 라벨에서 .env 파일용 CONSTANT_CASE 키 생성
  • 티켓의 기능 설명에서 PascalCase 클래스명을 즉시 생성
  • 리팩터링 중 일관되지 않은 변수명을 표준화

자주 묻는 질문

Q. camelCase와 PascalCase의 차이는 무엇인가요?

A. 둘 다 단어를 붙여 쓰지만 camelCase는 첫 글자가 소문자(myVariable), PascalCase는 첫 글자가 대문자(MyClass)입니다. 클래스, 타입, 컴포넌트에는 PascalCase를 씁니다.

Q. URL, ID 같은 약어는 어떻게 처리하나요?

A. 구글/마이크로소프트 등 최신 스타일 가이드는 약어도 한 단어로 취급합니다: parseUrl, userId. 본 도구도 이 규칙을 따라 왕복 변환이 일관됩니다.

Q. CONSTANT_CASE 입력에서도 단어 경계를 잘 인식하나요?

A. 네. 언더스코어, 하이픈, 공백, 대소문자 전환을 모두 단어 경계로 인식하므로 HELLO_WORLD, helloWorld, hello-world 모두 동일한 단어 목록으로 분해됩니다.

Q. 비영문 문자도 변환되나요?

A. 대소문자가 있는 문자는 정상 변환됩니다. 한국어/일본어/중국어처럼 대소문자가 없는 문자는 그대로 두고 구분자만 조정합니다.