Tailwind CSS 임의값(Arbitrary Value) 변환기
CSS 선언을 Tailwind 클래스로 바꿔서 복사
선언을 줄마다 붙여 넣으세요. 규칙 전체(.card { … })를 붙여 넣어도 됩니다.
Tailwind 클래스
임의값은 기능이 아니라 탈출구다
Tailwind의 임의값 문법을 처음 알게 되면 해방감이 옵니다. text-[13px], mt-[7px], bg-[#6366f1] — 디자인 시안의 어떤 숫자든 그대로 옮길 수 있습니다. 클래스 이름을 외울 필요도, 설정 파일을 열 필요도 없습니다.
그런데 그 해방감이 정확히 Tailwind가 막으려던 것입니다.
text-xs부터 text-9xl까지 열세 단계뿐인 폰트 크기 스케일은 게으름이 아니라 제약입니다. 스케일이 있으면 개발자는 12냐 14냐만 고르면 됩니다. 스케일이 없으면 12, 13, 13.5, 14가 전부 가능하고, 6개월 뒤 코드베이스에는 미묘하게 다른 폰트 크기가 열한 종류 생깁니다. 아무도 그걸 의도하지 않았습니다. 그냥 매번 시안에 적힌 숫자를 옮겼을 뿐입니다.
그래서 text-[13px]을 쓰고 있다면 던져야 할 질문은 “이 문법이 맞나?”가 아니라 “왜 우리 시스템에 13px 단계가 없지?”입니다. 답은 대개 셋 중 하나입니다.
- 13px이 반복해서 나온다 → 스케일에 단계가 빠진 것입니다. 테마에 추가하세요. v4라면
@theme블록에 한 줄입니다. - 13px이 여기 한 번만 나온다 → 시안이 흔들린 것입니다. 12나 14로 맞추면 아무도 차이를 알아채지 못합니다. 진짜입니다 — 1px 차이를 눈으로 잡아내는 사람은 그 화면을 만든 사람뿐입니다.
- 진짜로 여기만 13px이어야 한다 → 이때가 임의값이 정확한 도구인 순간입니다. 히어로 배너 하나, 서드파티 위젯에 맞춰야 하는 오프셋 하나. 이런 건 테마에 넣으면 오히려 오염입니다.
위 변환기가 margin-top: 24px을 받고 mt-[24px]이 아니라 mt-6을 먼저 내미는 이유가 이것입니다. 간격 스케일은 한 단계가 4px이라 1=4px, 2=8px, 4=16px, 6=24px입니다. 시안의 숫자를 4로 나눠 떨어지면 이미 이름이 있습니다. 셋 중 하나에 해당하는지 스스로 답해 보고 임의값을 쓰세요.
Tailwind는 JavaScript를 실행하지 않는다
Tailwind를 쓰는 사람이 가장 많이 겪는 버그이고, 원인을 알기 전까지 가장 이해가 안 가는 버그입니다.
// 아무 스타일도 안 나온다
<p className={`text-[${size}px]`}>...</p>
<div className={`mt-${spacing}`}>...</div>
<span className={`bg-${color}-500`}>...</span>오류도 없고 경고도 없습니다. 빌드는 통과하고, 화면만 스타일이 없습니다. 개발자 도구를 열면 클래스 이름은 text-[13px]로 멀쩡히 붙어 있는데, 그 클래스에 해당하는 CSS 규칙이 스타일시트에 아예 없습니다.
이유는 단순합니다. Tailwind는 빌드 시점에 소스 파일을 텍스트로 훑는 정적 스캐너입니다. 코드를 실행하지 않고, 타입도 보지 않고, 그냥 파일 안에서 클래스처럼 생긴 문자열을 찾습니다. 그리고 찾은 것들에 대해서만 CSS를 만듭니다.
스캐너가 `text-[${size}px]`을 만나면 거기서 보이는 건 text-[와 px]이라는 부스러기뿐입니다. size가 런타임에 13이 될지 20이 될지 알 방법이 없습니다 — 그걸 알려면 앱을 실행해야 하는데, 빌드 도구는 앱을 실행하지 않습니다. 그래서 아무것도 만들지 않습니다.
해결책은 완성된 클래스 문자열이 소스에 그대로 존재하게 만드는 것입니다.
// 방법 1: 전체 문자열 룩업 — 스캐너가 "mt-2"와 "mt-4"를 그대로 본다
const SPACING = { sm: "mt-2", md: "mt-3", lg: "mt-4" } as const;
<div className={SPACING[size]}>...</div>
// 방법 2: CSS 변수 — 값은 런타임에, 클래스는 빌드 시점에 고정
<div className="mt-[var(--gap)]" style={{ "--gap": `${n}px` }}>...</div>이 규칙은 지금 보고 있는 사이트의 코드에도 그대로 적용돼 있습니다. 상단 앱 메뉴의 아이콘 강조색은 플랫폼마다 다른데, 소스에는 { hub: "text-brand-2", conv: "text-brand" }처럼 완성된 클래스 리터럴이 적혀 있습니다. `text-${accent}`로 썼다면 메뉴 아이콘이 전부 무채색으로 나왔을 것입니다.
밑줄 규칙과 그 가장자리
클래스 이름에는 공백이 들어갈 수 없습니다. 공백이 있으면 브라우저가 거기서 클래스를 둘로 쪼갭니다. 그래서 Tailwind는 임의값 안의 _를 공백으로 되돌려 읽기로 정했습니다.
font-family: Times New Roman → font-[Times_New_Roman]
grid-template-columns: 1fr 200px → grid-cols-[1fr_200px]
box-shadow: 0 0 0 1px red → shadow-[0_0_0_1px_red]대가는 진짜 밑줄입니다. 값 안에 밑줄이 원래 있어야 한다면 백슬래시로 탈출시켜야 합니다 — var(--my_var)는 text-[var(--my\_var)]입니다. 이걸 빼먹으면 Tailwind가 var(--my var)라는 무효한 CSS를 만들고, 역시 조용히 아무 일도 일어나지 않습니다.
예외가 하나 있습니다. Tailwind는 url() 안의 밑줄은 건드리지 않습니다. 파일 경로에 밑줄이 흔하고, 거기서 공백으로 바뀌면 링크가 통째로 깨지기 때문입니다. bg-[url(/img/hero_bg.png)]는 그대로 동작합니다.
대괄호가 값 안에 들어가는 경우도 조심해야 합니다. [&[data-open]]:block 같은 선택자를 쓸 때 여는 괄호와 닫는 괄호의 짝이 맞아야 파서가 임의값의 끝을 찾습니다. 짝이 안 맞으면 파싱이 엉뚱한 데서 끊깁니다.
순서가 안 통하는 이유, 그리고 tailwind-merge
이건 거의 모든 사람이 한 번은 틀립니다.
<div className="p-4 p-2"> {/* p-2가 이길까? */}아닙니다. 클래스 문자열 안의 순서는 CSS와 아무 상관이 없습니다. class속성은 “이 요소는 이 클래스들에 속한다”는 집합일 뿐이고, 순서 정보가 아닙니다. 두 규칙의 명시도가 같을 때 이기는 쪽은 스타일시트에 나중에 나오는 규칙이고, 그 순서는 Tailwind가 유틸리티를 생성하면서 정합니다.
그래서 p-4 p-2와 p-2 p-4는 완전히 같은 결과를 냅니다. 어느 쪽이 이길지는 항상 정해져 있고, 여러분이 문자열을 어떻게 쓰든 바뀌지 않습니다.
이게 실무에서 터지는 지점은 컴포넌트에 클래스를 덧붙일 때입니다.
function Card({ className }) {
return <div className={`p-4 rounded-xl ${className}`} />;
}
<Card className="p-8" /> // p-8이 먹을 거라 기대하지만... 운에 맡기는 것이다해법은 순서를 바꾸는 게 아니라 충돌하는 클래스를 아예 지우는 것입니다. 이게 tailwind-merge가 존재하는 이유입니다. 이 라이브러리는 클래스 문자열을 파싱해서 같은 CSS 속성을 건드리는 것들 중 뒤에 온 것만 남기고 앞엣것을 제거합니다. 문자열 수준에서 처리하니까 CSS 순서와 무관하게 예측 가능해집니다.
twMerge("p-4 p-2") // → "p-2" (앞엣것을 지웠다)
twMerge("p-4 rounded-xl", "p-8") // → "rounded-xl p-8"이 사이트도 lib/utils.ts에 cn() 한 줄로 같은 걸 쓰고 있습니다 — twMerge(clsx(inputs)). Tailwind를 쓰면서 재사용 가능한 컴포넌트를 만든다면 거의 필수에 가깝습니다.
v3에서 v4로: 뭐가 실제로 달라졌나
검색해서 나오는 Tailwind 예제는 아직 대부분 v3입니다. 그런데 v4에서 바뀐 것 중 몇 개는 조용히 다른 결과를 냅니다.
- 설정이 CSS로 옮겨갔다:
tailwind.config.js가 사라지고@import "tailwindcss"아래@theme블록에 CSS 변수로 씁니다. 이 사이트도 그렇습니다 —--color-brand: var(--brand)한 줄이bg-brand·text-brand·border-brand를 전부 만듭니다. - content 배열이 없다: v3는 스캔할 파일 목록을 직접 나열해야 했고, 이걸 빠뜨리면 그 폴더의 클래스가 통째로 사라졌습니다. v4는 프로젝트를 자동으로 탐색합니다. 다만 정적 스캐너라는 성질 자체는 그대로입니다 — 위의 동적 클래스 문제는 v4에서도 똑같이 발생합니다.
- !important 위치가 뒤집혔다: v3의
!mt-4가 v4에서mt-4!입니다. CSS의!important가 값 뒤에 오는 것과 모양을 맞춘 것입니다. - 모서리 반경 이름이 한 칸 밀렸다: v3의
rounded-sm(2px)이 v4에서rounded-xs가 되고, 이름 없던rounded(4px)가rounded-sm이 됐습니다. 같은 클래스가 두 버전에서 다른 크기를 뜻합니다 — 마이그레이션할 때 소리 없이 어긋나는 대표적인 자리입니다. 위 변환기의 버전 선택이 이걸 반영합니다. - 간격 스케일이 동적이다: v3는 고정 목록이라
m-13이 없었지만, v4는--spacing의 배수를 요청받은 순간 만듭니다. v4에서margin: 52px은m-13이 되고, v3에서는m-[52px]일 수밖에 없습니다.
임의 속성 — 유틸리티가 아예 없는 경우
Tailwind가 유틸리티를 제공하지 않는 CSS 속성도 있습니다. 이때는 값이 아니라 속성 자체를 대괄호에 넣습니다.
[mask-type:luminance]
[--card-gap:12px]
[font-variation-settings:'wght'_600]강력하지만 안전망이 없습니다. Tailwind는 대괄호 안의 속성 이름이 진짜 CSS 속성인지 검사하지 않습니다. [colr:red]라고 쓰면 colr: red라는 CSS를 그대로 만들고, 브라우저는 그 선언을 조용히 버립니다. 오타 하나가 아무 흔적 없이 사라지는 것입니다. 위 변환기가 모르는 속성에 대해 임의 속성 문법을 ‘제안’으로만 보여 주고 결과 클래스 문자열에는 넣지 않는 이유입니다.
text-[...]가 색인지 크기인지
text- 하나가 폰트 크기와 글자색을 동시에 담당하는 건 원래 모호합니다. Tailwind는 값의 모양을 보고 추론합니다 — text-[13px]은 길이니까 폰트 크기, text-[#6366f1]은 색상 표기니까 글자색입니다.
추론이 안 되는 경우가 CSS 변수를 넣을 때입니다. text-[var(--x)]만 보고는 --x가 크기인지 색인지 알 수 없습니다. 이때는 데이터 타입 힌트를 직접 붙입니다.
text-[length:var(--font-size)] /* 폰트 크기다 */
text-[color:var(--fg)] /* 글자색이다 */힌트를 빼먹으면 Tailwind가 한쪽으로 찍고, 절반의 확률로 틀린 속성이 나옵니다. 그리고 여기까지 왔다면 다시 첫 번째 이야기로 돌아갈 차례입니다 — 그 변수를 테마에 등록해서 text-brand로 부를 수는 없는지.
자주 묻는 질문
- text-[13px]처럼 임의값을 써도 되나요?
- 문법적으로는 완전히 정상이고 Tailwind가 공식적으로 지원하는 기능입니다. 다만 임의값은 '탈출구'로 설계된 것이라, 자주 쓰고 있다면 그건 값이 특이해서가 아니라 디자인 시스템에 그 값이 없다는 신호입니다. 13px이 팀에서 반복해서 나온다면 테마에 13px 단계를 추가하거나, 12px·14px 중 하나로 합의하는 편이 낫습니다. 한 번만 쓰이는 히어로 배너의 어긋난 값 같은 곳에는 임의값이 정확한 도구입니다.
- `text-[${size}px]`처럼 변수를 넣었는데 스타일이 아예 안 먹습니다.
- Tailwind는 빌드 시점에 소스 파일을 텍스트로 훑어서 완성된 클래스 문자열을 찾습니다. JavaScript를 실행하지 않기 때문에 템플릿 리터럴 안의 ${size}가 무엇이 될지 알 수 없고, 그래서 해당 CSS를 아예 생성하지 않습니다. 오류도 나지 않고 그냥 조용히 스타일이 없습니다. 해결책은 완성된 클래스를 값으로 갖는 매핑 객체({ sm: 'mt-2', lg: 'mt-4' })를 만들거나, CSS 변수를 인라인 스타일로 넘기고 mt-[var(--x)]로 받는 것입니다.
- 임의값에 공백이 들어가는 값은 어떻게 쓰나요?
- 밑줄로 바꿉니다. font-family: Times New Roman은 font-[Times_New_Roman]이 되고, Tailwind가 CSS를 생성할 때 밑줄을 공백으로 되돌립니다. 클래스 이름에는 공백이 들어갈 수 없기 때문에 생긴 규칙입니다. 값에 진짜 밑줄이 들어가야 한다면 백슬래시로 탈출시켜야 합니다 — var(--my_var)는 text-[var(--my\_var)]입니다.
- !important는 v3와 v4에서 어떻게 다른가요?
- 위치가 반대입니다. v3는 클래스 앞에 붙여 !mt-4로 쓰고, v4는 뒤에 붙여 mt-4!로 씁니다. v3에서 음수 클래스에 붙일 때는 느낌표가 부호보다 앞이라 !-mt-4가 됩니다. 이 변환기 위쪽의 버전 선택을 프로젝트에 맞게 바꾸면 해당 버전 문법으로 출력합니다.
- p-4 p-2처럼 같은 속성을 두 번 쓰면 뒤엣것이 이기나요?
- 아닙니다. 클래스 문자열 안의 순서는 CSS에 아무 영향도 주지 않습니다. 두 클래스의 명시도가 같으므로 스타일시트에 나중에 등장하는 규칙이 이기고, 그 순서는 Tailwind가 정합니다. p-4와 p-2 중 무엇이 이길지는 문자열을 어떻게 쓰든 항상 같습니다. 그래서 조건부로 클래스를 덮어써야 한다면 tailwind-merge 같은 라이브러리로 충돌하는 클래스를 아예 제거해야 합니다.
- v4에서는 tailwind.config.js가 없는데 스케일을 어떻게 바꾸나요?
- CSS 파일 안에서 @theme 블록으로 정의합니다. 간격 스케일은 --spacing 하나가 기준이고(기본 0.25rem), 색상은 --color-brand: #6366f1처럼 선언하면 bg-brand·text-brand 같은 유틸리티가 자동으로 생깁니다. content 배열도 사라져서 소스 스캔 대상을 직접 나열하지 않습니다. 반대로 말하면 v4에서 테마에 값을 추가하는 비용이 훨씬 싸졌기 때문에, 임의값으로 때우는 대신 스케일에 넣을 이유가 더 커졌습니다.