본문 바로가기
Converter

케이스 변환기 (camelCase · snake_case · kebab-case)

변수 표기법을 한 번에 전부 변환

여러 줄을 붙여 넣으면 줄 단위로 각각 변환합니다.

변환할 이름을 입력하세요.

변환 결과

camelCaseJS·Java 변수·함수
PascalCase클래스·React 컴포넌트·타입
snake_caseDB 컬럼·Python·JSON 키
kebab-caseURL·CSS 클래스·파일명
CONSTANT_CASE상수·환경 변수
Title Case제목·라벨
sentence case문장·설명문

이 도구가 실제로 하는 일은 ‘자르기’다

user_nameuserName으로 바꾸는 일은 두 단계입니다. 먼저 단어로 자르고, 그다음 규칙대로 다시 붙입니다. 두 번째 단계는 정말 아무것도 아닙니다 — 소문자로 눕혀서 _로 잇거나, 첫 글자를 대문자로 올려서 그냥 붙이면 끝입니다.

어려운 건 전부 첫 번째 단계에 있습니다. user_name처럼 구분자가 이미 있으면 자를 곳이 눈에 보이지만, getHTTPResponse에는 구분자가 없습니다. 사람은 “get / HTTP / Response”라고 바로 읽지만, 그건 HTTP가 약어라는 걸 이미 알고 있어서입니다. 프로그램은 대문자가 네 개 연속으로 있다는 사실만 압니다.

순진한 정규식이 만드는 결과

camelCase를 snake_case로 바꾸는 코드는 검색하면 대개 이렇게 나옵니다. 대문자 앞에 _를 넣고 전부 소문자로 내리는 방식입니다.

s.replace(/([A-Z])/g, "_$1").toLowerCase()

"userName"        →  "user_name"        ✓
"getHTTPResponse" →  "get_h_t_t_p_response"   ✗

user_name은 맞습니다. 그래서 이 코드는 대부분의 테스트를 통과하고, 배포되고, 몇 달 뒤에 누군가 parseXMLFile을 넣었을 때 parse_x_m_l_file이라는 컬럼명을 마이그레이션에 만들어 냅니다.

고치려면 규칙이 필요합니다. 대문자가 연속으로 이어지다가 소문자가 나오면, 마지막 대문자 앞에서 자른다. 마지막 대문자는 약어의 일부가 아니라 다음 단어의 첫 글자이기 때문입니다.

"getHTTPResponse"
      ^^^^^^
      HTTPR + 소문자 e  →  R 은 약어가 아니라 Response 의 R 이다
      따라서: get / HTTP / Response

정규식으로는 [A-Z]+(?!.[a-z]) 같은 부정 전방탐색으로 표현합니다. 이제 getHttpResponse, parseXmlFile, ioError가 제대로 나옵니다.

그런데 이 규칙도 진다

규칙을 하나 더 넣었으니 이제 끝일까요? 아닙니다. 아래 이름들은 전부 실제로 코드에 존재하고, 전부 위 규칙을 빠져나갑니다.

  • iOS — 소문자로 시작하는고유명사입니다. ‘i’와 ‘OS’가 갈립니다. 애플이 정한 표기라 규칙으로 어쩔 수가 없습니다.
  • IPv6— 약어 한복판에 소문자가 끼어 있습니다. ‘IP’ + ‘v6’이 한 덩어리인데, 어떤 규칙도 이걸 알 수 없습니다.
  • OAuth2, S3 — 대문자·소문자·숫자가 규칙 없이 섞여 있습니다.

이건 구현이 게을러서가 아닙니다. 어휘 지식이 필요한 문제를 문자열 패턴으로 풀려고 하기 때문에 생기는 한계입니다. 완벽하게 하려면 약어 사전이 있어야 하고, 사전이 있어도 그 사전에 없는 사내 약어가 내일 추가됩니다.

그래서 구글 자바 스타일 가이드는 방향을 뒤집습니다. “약어를 살리는 규칙을 잘 만들자”가 아니라 “약어를 보통 단어처럼 취급하라”는 쪽입니다. HTTPResponse가 아니라 HttpResponse, parseXMLFile이 아니라 parseXmlFile로 쓰라는 것입니다. 보기에 어색할 수 있지만, 이렇게 하면 애초에 자를 곳이 명확해져서 위 문제가 통째로 사라집니다. 이 변환기가 getHttpResponse를 내놓는 이유도 같습니다.

변환은 되돌릴 수 없다

여기서 많은 사람이 놓치는 지점이 있습니다. 케이스 변환은 단방향입니다.

"HTTPResponse"  →  snake  →  "http_response"
"http_response" →  pascal →  "HttpResponse"   ← 원래 이름이 아니다

소문자로 눕히는 순간 “여기는 약어였다”는 정보가 없어집니다. http_response만 보고 원래가 HTTPResponse였는지 HttpResponse였는지 알아낼 방법은 없습니다. 정보가 파일에 없기 때문입니다.

이게 실무에서 무는 지점은 일괄 변환 스크립트입니다. 코드베이스 전체를 훑어 이름을 바꾸는 codemod를 돌렸는데 중간에 실패해서 다시 돌렸다고 해봅시다. 이미 변환된 부분은 다시 변환해도 안전할까요? 대부분은 그렇습니다(userId userId). 하지만 “안전하게 두 번 돌릴 수 있다”와 “되돌릴 수 있다”는 전혀 다른 보장입니다. 후자는 없습니다. 변환 전 커밋을 남겨 두는 것 말고는 복구 수단이 없습니다.

왜 언어마다 표기법이 다른가

“그냥 하나로 통일하면 안 되나”라는 생각이 들 수 있는데, 각 관습에는 대체로 기술적 이유가 있습니다. 취향 문제가 아닙니다.

DB가 snake_case인 이유: SQL은 대소문자를 구분하지 않는다

이게 가장 결정적입니다. SQL에서 따옴표 없는 식별자는 한 가지 케이스로 접힙니다. PostgreSQL은 소문자로, Oracle은 대문자로 접습니다.

CREATE TABLE users (userName text);   -- 실제로는 username 으로 만들어진다
SELECT userName FROM users;           -- 된다 (username 을 찾는다)
SELECT USERNAME FROM users;           -- 이것도 된다. 전부 같은 컬럼이다

CREATE TABLE users ("userName" text); -- 이제 진짜 userName 이다
SELECT userName FROM users;           -- ✗ column "username" does not exist

따옴표를 한 번 쓰면 영원히 써야 합니다. 모든 쿼리, 모든 마이그레이션, 모든 ORM 설정에서요. 하나라도 빠뜨리면 런타임에 터집니다. 대문자로 단어를 구분하는 방식이 애초에 불가능한 환경이니, 밑줄로 구분하는 것 말고 선택지가 없었던 셈입니다.

CSS가 kebab-case인 이유

CSS 속성명(font-size, background-color)이 하이픈인 것은 초기 CSS 식별자 문법에서 온 관습입니다. 그리고 이건 JS와 만나는 지점에서 그대로 문제가 됩니다 — el.style.font-size는 문법 오류입니다(빼기로 파싱됩니다). 그래서 DOM API는 el.style.fontSize로 camelCase를 씁니다. 하나의 속성이 사는 곳에 따라 두 이름을 갖는 것입니다.

상수가 SCREAMING_SNAKE인 이유

C의 매크로 관습에서 왔습니다. #define MAX_SIZE 100은 컴파일 전에 텍스트가 그대로 치환되는 것이라 변수와 성질이 완전히 다르고, 잘못 건드리면 이상한 방식으로 깨집니다. 그래서 “이건 매크로다, 조심해라”를 눈에 띄게 표시하는 관습이 생겼고, 매크로가 없는 언어들도 “이건 바뀌지 않는 값”이라는 신호로 이 표기만 가져다 씁니다.

API 경계 — 자동 변환 미들웨어를 조심하라

위 이유들이 겹치면 아주 흔한 상황이 만들어집니다. DB는 snake_case, 자바스크립트는 camelCase. 그 사이 어딘가에서 created_at createdAt이 되어야 합니다.

가장 유혹적인 해법은 미들웨어입니다. 응답 JSON의 모든 키를 재귀적으로 훑어서 자동으로 바꿔 버리는 것이죠. 한 줄이면 되고, 아무도 신경 쓸 필요가 없어집니다. 그리고 이게 바로 문제입니다.

  • 데이터인 키까지 망가뜨립니다.사용자가 정의한 태그 맵, 국가 코드 맵, 설문 문항 ID를 키로 쓰는 객체가 응답에 하나라도 있으면, 미들웨어는 그것도 ‘키’니까 똑같이 변환합니다. user_defined_key는 컬럼명이 아니라 사용자가 입력한 값인데 userDefinedKey가 되어 나갑니다. 미들웨어는 스키마를 모르니 구별할 방법이 없습니다.
  • 왕복이 안 됩니다. 앞에서 본 그대로입니다. 요청에서 역변환할 때 userIDuser_id가 되지만 user_i_d가 될 수도 있고, 어느 쪽이든 DB 컬럼과 맞는지는 운입니다.
  • 디버깅이 어렵습니다. 네트워크 탭에서 본 키와 코드에서 쓰는 키가 다르고, 로그의 키와 DB의 키가 또 다릅니다. 이름으로 검색이 안 되는 코드베이스가 됩니다.

더 나은 방법은 경계에서 명시적으로 매핑하는 것입니다. 쿼리 결과를 도메인 객체로 만드는 함수를 하나 두고 거기서 손으로 적습니다. 지루하지만, 타입 검사를 받고, 검색되고, 변환할 것과 하지 않을 것을 사람이 정합니다. 이 변환기는 그 매핑 코드를 처음 작성할 때 컬럼 목록을 붙여 넣어 쓰라고 만든 도구입니다 — 런타임에 매번 돌리라고 만든 게 아닙니다.

대소문자만 바꾸는 것이 위험한 곳: 파일 이름

마지막으로 케이스 문제 중 가장 조용히 사람을 잡는 것 하나. MyComponent.tsx mycomponent.tsx는 같은 파일일까요?

운영체제에 따라 다릅니다. macOS(APFS 기본 설정)와 윈도우(NTFS)는 대소문자를 구분하지 않습니다. 리눅스는 구분합니다. 그래서 이런 일이 벌어집니다.

// 로컬(macOS): 잘 된다. 파일 시스템이 알아서 찾아 준다.
import Button from "./components/button";   // 실제 파일은 Button.tsx

// CI(리눅스): 빌드 실패
//   Module not found: Can't resolve './components/button'

내 컴퓨터에서는 되는데 CI에서만 깨지는 전형적인 사례입니다. 반대 방향도 있습니다. 파일명 대소문자만 바꾸면 git이 변경을 알아채지 못해서, 커밋은 됐는데 리모트에는 옛 이름이 그대로 남습니다. 확실하게 하려면 임시 이름을 거쳐 git mv를 두 번 해야 합니다.

git mv Button.tsx tmp.tsx
git mv tmp.tsx button.tsx

그래서 파일명 규칙은 프로젝트 초기에 정해서 지키는 게 최선입니다. 이미 섞여 있다면 한 번에 정리하지 말고 새로 만드는 파일부터 규칙을 적용하세요 — 파일명 일괄 변경 PR은 리뷰가 불가능하고, 리눅스 CI에서만 터지며, 되돌리기도 어렵습니다.

자주 묻는 질문

getHTTPResponse를 변환했더니 getHttpResponse가 됩니다. HTTP가 대문자로 유지되어야 하는 것 아닌가요?
의도한 동작입니다. 약어를 보통 단어처럼 취급하는 것은 구글 자바 스타일 가이드를 비롯한 여러 스타일 가이드의 선택이기도 합니다. 이유는 일관성입니다 — HTTP를 살려 두려면 '이 토큰은 약어였다'는 정보를 계속 들고 다녀야 하는데, snake_case로 한 번 눕힌 입력(http_response)에는 그 정보가 이미 없습니다. 그러면 같은 이름이 어디서 왔느냐에 따라 다른 결과가 나옵니다. 한쪽으로 통일하면 최소한 예측은 가능합니다.
camelCase로 바꿨다가 다시 원래대로 되돌릴 수 있나요?
일반적으로는 안 됩니다. 케이스 변환은 손실 변환입니다. HTTPResponse를 snake_case로 바꾸면 http_response가 되고, 여기서 camelCase로 되돌리면 HTTPResponse가 아니라 httpResponse가 나옵니다. 소문자로 눕히는 순간 원래 대소문자 정보가 사라지기 때문입니다. 코드 전체를 일괄 변환하는 스크립트를 짤 때 이 점이 중요합니다 — 실수로 두 번 돌려도 안전한 것과, 되돌릴 수 있는 것은 다른 이야기입니다.
DB 컬럼은 왜 snake_case를 쓰나요?
SQL 식별자가 대소문자를 구분하지 않기 때문입니다. 표준 SQL에서 따옴표 없는 식별자는 한 가지 케이스로 접히는데, PostgreSQL은 소문자로, Oracle은 대문자로 접습니다. 즉 userName이라고 만들어도 실제로는 username으로 저장되고, camelCase가 눈에 보이지 않게 됩니다. 케이스를 유지하려면 큰따옴표로 감싸야 하고, 그러면 이후 모든 쿼리에서 영원히 감싸야 합니다. 단어 경계를 대문자 대신 밑줄로 표시하는 snake_case는 이 문제를 아예 피해 갑니다.
여러 줄을 한 번에 변환할 수 있나요?
네. 여러 줄을 붙여 넣으면 줄 단위로 각각 변환합니다. DB 컬럼 목록이나 JSON 키 목록을 한 열 통째로 복사해 오는 경우가 가장 흔해서 그렇게 만들었습니다. 빈 줄은 빈 줄로 남기므로 입력과 출력의 줄 번호가 어긋나지 않고, 결과를 그대로 스프레드시트나 에디터에 붙여 넣을 수 있습니다.
iOS나 IPv6 같은 이름이 이상하게 변환됩니다.
맞습니다. 정직하게 말하면 이건 규칙으로 풀리지 않는 문제입니다. 이 도구는 '대문자가 연속되다가 소문자가 나오면 마지막 대문자 앞에서 자른다'는 규칙을 쓰는데, iOS는 소문자로 시작하고 IPv6은 약어 중간에 소문자가 끼어 있어서 둘 다 규칙 밖입니다. 입력창 아래에 분해된 단어를 그대로 보여주는 이유가 이것입니다 — 결과가 이상하면 경계가 어디서 잘못 잡혔는지 눈으로 확인하고 직접 고치는 편이 빠릅니다.
파일명을 MyComponent.tsx에서 mycomponent.tsx로 바꿔도 되나요?
조심해야 합니다. macOS와 윈도우의 기본 파일 시스템은 대소문자를 구분하지 않아서 두 이름이 같은 파일이지만, 리눅스는 구분합니다. 그래서 로컬에서는 멀쩡한데 리눅스 CI에서 'Cannot find module'로 빌드가 깨지는 일이 생깁니다. git도 대소문자만 바뀐 변경을 놓치는 경우가 있어서, 임시 이름을 거쳐 git mv를 두 번 하거나 git config core.ignorecase false를 쓰는 편이 안전합니다.