컴파일되고 타입이 지정된 CSS의 필요성 (AI의 잘못을 비난하며)
요약
AI가 생성하는 CSS 코드의 환각(hallucination)과 의미론적 오류 문제를 지적하며, 이를 해결하기 위해 TypeScript와 같은 타입 시스템을 CSS에 도입해야 한다고 주장합니다. AI는 패턴을 근사치로 계산하기 때문에 검증되지 않은 CSS 값을 생성할 위험이 크며, 빌드 타임의 타입 체크가 필수적임을 강조합니다.
핵심 포인트
- AI 생성 코드의 94%가 타입 체크 실패로 인한 오류 발생
- CSS는 구문 오류는 잡지만 의미론적 오류 검증이 어려움
- AI는 존재하지 않는 CSS 값이나 잘못된 디자인 토큰을 자신 있게 생성함
- 빌드 타임에 타입이 지정된 스타일(typed styles) 도입 필요성
우리는 TypeScript를 지금의 수준으로 만들기 위해 수년의 시간을 보냈습니다. TypeScript는 이미 여러분의 API, 컴포넌트, 상태(state)를 체크합니다. 하지만 여러분의 CSS 값들은 여전히 아무것도 컴파일하지 못하는 문자열(strings)일 뿐입니다. 이미 가지고 있는 도구가 오늘 당장 이 일을 할 수 있는데, 왜 새로운 도구를 기다리시나요?
2025년의 한 학술 연구 [1]에 따르면, LLM(대규모 언어 모델)이 생성한 컴파일 오류의 94%가 타입 체크(type-check) 실패로 나타났습니다. GitHub의 Octoverse 보고서 [2]에서도 TypeScript가 플랫폼에서 가장 많이 사용되는 언어로 부상한 이유를 설명하며 동일한 통계를 인용했습니다. 타입이 지정된 언어(typed languages)에서는 컴파일러가 AI가 틀린 대부분의 부분을 잡아냅니다. 하지만 CSS에는 컴파일러가 없습니다.
다음의 두 가지 주장이 있으며, 이들은 분리될 수 있습니다. 첫째: CSS에는 검증 계층(verification layer)이 부족하며, AI는 그 격차로 인해 비용을 발생시킵니다. 둘째: 빌드 타임에 타입이 지정된 스타일(build-time typed styles)이 제가 내린 해결책입니다. TypeScript는 이미 스택(stack)에 포함되어 있습니다. 그 외의 것은 또 다른 의존성(dependency)이자 또 다른 드리프트(drift)의 원인이 됩니다. 첫 번째 주장은 수용하되 두 번째 주장에 대해서는 반론을 제기할 수 있을 것입니다.
AI는 CSS에서 실패한다
var(--spacign-md)라고 작성해도 아무것도 실패하지 않습니다. 브라우저는 조용히 폴백(fallback)합니다. 디자인 토큰(design token)이 16px인데 padding: 12px라고 작성해도, 그대로 렌더링됩니다. 그리고 배포됩니다.
AI는 존재하지 않는 값인 width: fit-parent [3]를 생성했습니다 (실제 값은 fit-content입니다). 디자인 토큰이 16px인 spacing-md일 때 padding: 12px라고 작성하기도 합니다 [4]. AI는 여러분의 토큰 파일을 확인하는 것이 아니라 근사치(approximates)를 계산하기 때문입니다. v4 프로젝트에 Tailwind v3 로직을 적용하여 [5], 폐기된(deprecated) 패키지를 가져오고 스타일을 깨뜨리기도 합니다. 한 필자가 표현했듯이: "AI가 이 문제를 만든 것이 아닙니다. AI는 이 문제를 확장시켰을 뿐입니다." [4]
인간 개발자는 불확실할 때 주저합니다. 하지만 AI는 그렇지 않습니다. AI는 알려진 패턴을 구현할 때나 존재하지 않는 것을 환각(hallucinating)할 때나 똑같은 자신감으로 코드를 생성합니다 [3].
PostCSS나 Stylelint 같은 린터(Linters)는 구문 오류(syntax errors)는 잡아내지만, 의미론적 오류(semantic errors)는 잡아내지 못합니다. 그것들은 문법을 검증합니다. 부족한 것은 의미를 검증할 수 있는 무언가입니다. 맞습니다, AI는 CSS-in-JS보다 일반 CSS를 더 잘 작성합니다. 더 많은 학습 데이터가 있기 때문입니다. 하지만 검증되지 않은 더 나은 CSS는 여전히 검증되지 않은 상태일 뿐입니다.
"AI는 그냥 더 좋아질 것이다"
그럴지도 모릅니다. 하지만 우리는 여전히 오늘 코드를 배포해야 합니다.
실제 웹 개발 작업의 벤치마크인 Web-Bench [6] (ByteDance, 2025)에 따르면, 당시 선두 모델이었던 Claude 3.7 Sonnet의 1차 통과 정확도(first-pass accuracy)는 25.1%에 불과했습니다. GitClear의 2025년 분석 [7] (2020-2024년 사이의 2억 1,100만 줄의 변경 사항 대상)에 따르면, 복사해서 붙여넣은 코드(copy-pasted code)는 8.3%에서 12.3%로 증가한 반면, 리팩터링(refactoring)은 25%에서 10% 미만으로 급감했습니다. 벤치마크 점수는 올라가고 있지만, 실제 품질 지표는 반대 방향으로 가고 있습니다.
Simon Willison은 [8] 코드 환각(hallucinations)은 컴파일러가 이를 잡아낼 수 있기 때문에 가장 덜 위험한 종류라고 주장했습니다. 그의 말이 맞습니다. 다만 CSS에는 컴파일러가 없을 뿐입니다. 그의 낙관론에는 CSS 모양의 구멍이 뚫려 있습니다.
"AI가 더 좋아질 것이다"라는 말은 도박입니다. 타입 시스템(type system)은 보증입니다.
Tailwind의 현주소
Tailwind는 주간 다운로드 수가 약 1,200만 회 [9]에 달하며 신규 프로젝트에서 압도적인 위치를 차지하고 있습니다. 이 프레임워크의 LSP(Language Server Protocol)는 잘못된 클래스를 표시하고, ESLint 플러그인은 스케일(scale) 사용을 강제합니다.
하지만 Tailwind v4는 설정을 JavaScript (tailwind.config.ts)에서 네이티브 CSS (@theme {} 블록)로 이동시켰습니다. 더 단순해진 것은 맞습니다. 하지만 이제 기본 경로는 타입 시스템(type system) 외부에서 토큰(tokens)을 CSS 문자열로 직접 작성하는 방식입니다. 여전히 타입이 지정된 소스(typed source)로부터 @theme를 생성할 수 있으며, 이것이 바로 제가 아래에서 주장할 방향이기도 하지만, v4의 어떤 기능도 사용자가 그 방향으로 가도록 유도하지 않습니다. 저항이 가장 적은 경로(path of least resistance)가 타입을 잃어버린 것입니다. 만약 디자이너가 #2563eb를 지정했는데 AI가 @theme { --color-brand: #3b82f6 }라고 작성한다면, CSS 빌드는 문제없이 통과됩니다. 둘 다 유효한 16진수(hex)이기 때문입니다. 문제는 구문(syntax)이 아니라, 스타일과 TypeScript 컴포넌트 사이에 계약(contract)이 없다는 점입니다. 한편, Tailwind Labs 자체도 AI의 압박을 받고 있습니다. 매출은 80% 감소했고, 엔지니어 3명이 해고되었습니다 [10]. 회사는 허둥대고 있지만 프레임워크는 번창하고 있습니다. AI는 아무도 계획하지 않은 방식으로 가장 인기 있는 CSS 프레임워크의 생태계마저 재편하고 있습니다.
빌드 타임 타입 지정 스타일 (Build-time typed styles)
런타임 CSS-in-JS가 아닙니다. styled-components, Emotion, 런타임 스타일 주입(runtime style injection): 이것들은 타당한 이유로 이미 끝났으며 [11], 계속 끝난 상태로 남아있어야 합니다.
이것은 다른 이야기입니다. 값들이 TypeScript에 존재하며, 컴파일러(compiler)에 의해 검사되고, 정적 CSS(static CSS)를 생성합니다. 런타임(runtime)은 제로입니다. 출력물은 순수 CSS입니다. 검증은 브라우저에 도달하기 전에 이루어집니다. AI는 출력을 생성하기 위해서라도 반드시 TypeScript 명세(spec)를 충족해야만 합니다.
그 위에 사용자 정의 검증(custom validation)을 작성할 수도 있습니다. 예를 들어 색상 쌍이 대비 요구 사항을 충족하는지, 측정값이 범위를 초과하지 않는지 등을 단언(assert)할 수 있습니다. 만약 당신의 값들이 이미 TypeScript에 존재한다면, 왜 Sass에서 병렬적인 세트를 별도로 유지해야 합니까?
왜 두 세트의 값을 유지해야 하는가?
TypeScript는 2025년 8월 기준 GitHub에서 가장 많이 사용되는 언어입니다 [2]. 당신의 간격 스케일(spacing scale), 브레이크포인트(breakpoints), 테마 설정(theme config) 등은 이미 .ts 파일에 있습니다.
질문은 "CSS를 JavaScript에 넣어야 하는가?"가 아닙니다. "이미 존재하는 값들에 타입을 지정해야 하는가?"입니다.
CSS 변수(CSS variables)를 TypeScript 토큰(tokens)과 별개로 유지한다면, 동기화해야 할 두 세트의 값이 생깁니다. 바로 그 지점에서 드리프트(drift, 불일치)가 스며듭니다. TS에서 토큰을 한 번만 정의하고, 이를 CSS 변수, Tailwind 테마, 반응형 헬퍼(responsive helpers) 등 프로젝트에 필요한 무엇으로든 출력하세요. 하나의 소스(one source), 동기화 문제 없음.
CSS 변수는 훌륭합니다. 하지만 계약(contract)은 아닙니다.
저는 예전에 CSS 변수를 무시하곤 했습니다. 그러다 그것들의 실제 용도를 발견했습니다. 창 크기(window sizes)를 위한 React 컨텍스트(context)를 대체하는 Figma 디자인 토큰으로부터의 반응형 값들이었습니다. 엄청난 단순화였습니다. 저는 CSS 변수의 사용을 그것들이 진정으로 가치를 증명하는 곳으로 제한하는 것에 엄격합니다.
하지만 CSS 변수는 AI와 관련하여 특정한 약점을 가지고 있습니다. var(--spacign-md)는 문법적으로는 유효하지만 조용히 실패(silently fails)하는 구문입니다. 변수가 루트(root)에서 설정되고, 레이아웃 컴포넌트에서 재정의되고, 카드에서 다시 재정의된 후, 여러 계층 아래의 버튼에서 소비될 때, AI는 어느 단계에서 값이 설정되었는지 추론할 방법이 없습니다. @property는 네이티브 타입 검사(native type checking)를 추가하지만, 이는 빌드 타임(build time)이 아닌 렌더 타임(render time)에 검증됩니다. 잘못된 값이 여전히 배포됩니다. 반대 방향으로 생각해보면, getComputedStyle을 사용하여 JavaScript에서 CSS 변수를 읽는 것은 런타임 문자열 파싱(runtime string parsing)입니다. 들어올 때 타입 안전성(type safety)이 없고, 나갈 때도 타입 안전성이 없습니다.
그리고 네이티브 CSS 기능은 브라우저 지원 측면에서 항상 뒤처집니다. 값이 TypeScript에 존재하고 정적 CSS로 컴파일될 때, 여러분은 출력물을 제어할 수 있습니다. 브라우저 호환성(browser compatibility)은 아키텍처의 문제가 아니라 빌드(build)의 문제가 됩니다.
타입이 지정된 토큰(typed tokens)으로부터 CSS 변수(CSS variables)를 여전히 출력할 수 있습니다. 이것은 절충(tradeoff)이 아니라, 바로 그것이 핵심입니다. 런타임(runtime)의 CSS와 빌드 타임(build time)의 TypeScript 모두에서 볼 수 있는 하나의 타입 지정된 소스(typed source)를 갖는 것입니다.
작성(authoring)이 아닌 검증(verification)
이 논쟁은 작성(authoring)이 아니라 검증(verification)에 관한 것입니다. AI는 작성을 수월하게 만들었습니다. 여러분이 요청하는 만큼 빠르게 CSS를 생성해 줍니다. 하지만 올바른 CSS를 생성하는 것은 다른 문제이며, 아무도 검증(checking) 부분을 해결하지 못했습니다. "디자인 시스템은 어휘(vocabulary) 문제를 해결합니다. 하지만 검증 문제를 해결하지는 못합니다." [4] 타입(Types)은 바로 그 검증 계층(verification layer)입니다.
가장 강력한 반론은 "AI에게 더 나은 컨텍스트(context)를 제공하면 된다"는 것입니다. 이는 부분적으로 맞습니다. Sachin Patel의 팀 [12]은 Figma 토큰을 CSS 변수와 정렬하여 신뢰할 수 있는 출력을 얻어냈습니다. 저 또한 컨텍스트 엔지니어링(context engineering)을 사용합니다. AI가 코드베이스 표준을 따르도록 Claude Code 기술 및 Cursor 규칙을 작성했습니다. 이러한 접근 방식은 효과가 있습니다.
하지만 컨텍스트는 대화와 같습니다. 다음 개발자, 혹은 규칙이 로드되지 않은 다음 AI 세션은 이를 무시할 수 있습니다. 타입(Types)은 계약(contract)입니다. 타입은 컴파일을 통과해 넘어갈 수 없습니다. 또는 Builder.io의 블로그 포스트 [13]에서 표현했듯이: "타입이 없다면 AI는 추측하는 것이고, 타입이 있다면 AI는 명세(spec)를 읽는 것입니다."
스케일에서 벗어난 값(off-scale)이라도 가시적이기만 하면 괜찮습니다. 명시적인 m(17)은 Tailwind 클래스 문자열 속에 숨겨진 마법 같은 p-[17px]와는 다릅니다. 그리고 구조적으로 구별되기 때문에, 이를 위해 린트(lint)를 수행할 수 있습니다. 탈출구(escape hatches)가 자체적인 구문을 가지고 있다면, 스케일을 벗어난 밀도를 표시하는 CI 규칙을 만드는 것은 매우 간단합니다. 스케일을 벗어난 값과 스케일 내의 값이 동일해 보인다면, 그러한 규칙은 불가능합니다.
실제로 시각적 회귀 테스트 (Visual regression testing), 사람이 직접 검토하는 리뷰 (Human review), 다양한 뷰포트 (Viewports)에서의 레이아웃 확인 등은 타입이 지정된 스타일 (Typed styles)을 사용하든 아니든 수행해야 하는 작업입니다. 타입이 지정된 스타일이 변화시키는 것은 바로 '검토에 쏟는 시간의 내용'입니다. 타입이 없다면 검토자는 기계적인 오류와 시각적인 오류를 모두 잡아내야 합니다. 타입이 있다면 기계적인 레이어는 코드가 검토 단계에 도달하기 전에 처리됩니다. 남는 것은 판단(Judgment) 작업뿐입니다. 좋은 CSS를 작성하는 데에는 분류학자 (Taxonomist)적인 특성이 있습니다. 값을 분류하고, 요소가 어디에 속해야 하는지 결정하는 일 말입니다. 그 부분은 인간의 영역으로 남습니다. 타입은 소음을 제거하여 당신이 그 작업에 집중할 수 있게 해줍니다.
저는 CSS-Calipers를 통해 이 과정 중 타입이 지정된 측정 (Typed-measurement) 부분을 구축해 왔습니다. 그 레이어는 견고합니다. 컴파일 타임 단위 안전성 (Compile-time unit safety), 불변 값 (Immutable values), 그리고 보이지 않는 대신 명시적으로 드러나는 스케일 밖의 값 (Off-scale values) 등을 제공합니다. 이를 둘러싼 더 넓은 프레임워크는 아직 해결되지 않았으며, 저 또한 해결되었다고 주장하는 것이 아닙니다.
프로젝트에 필요한 것을 사용하세요
이것은 전부 아니면 전무 (All-or-nothing) 식의 제안이 아닙니다. 아무도 당신의 프로젝트를 CSS-in-JS로 다시 작성하라고 요구하지 않습니다. 어쩌면 전체 간격 스케일 (Spacing scale)에 타입을 지정할 수도 있고, 어쩌면 계속해서 문제를 일으키는 단 하나의 핵심 토큰 (Mission-critical token)에만 타입을 지정할 수도 있습니다. 혹은 이 모든 것이 전혀 필요하지 않을 수도 있습니다. 당신의 프로젝트를 가장 잘 아는 것은 당신입니다.
저 자신도 변화에 저항적인 편입니다. CSS-in-JS를 처음 접했을 때 본능적인 거부감을 느꼈습니다. CSS 변수 (CSS variables)를 접했을 때도 마찬가지였습니다. 두 번 모두, 특정 사용 사례 (Use case)가 제 마음을 돌려놓았습니다. 유행 때문이 아니라, 해당 도구가 대안들보다 문제를 더 잘 해결했기 때문입니다.
업계가 런타임 CSS-in-JS (Runtime CSS-in-JS)를 떠난 것은 옳은 결정이었습니다. 네이티브 CSS (Native CSS)는 그 어느 때보다 강력합니다. Tailwind가 지배적인 데에는 실질적인 이유가 있습니다. 제 요점은 그 무엇을 대체하자는 것이 아닙니다. 중요한 부분에 대해서는 검증 레이어 (Verification layer)가 존재해야 하며, 이를 생략할 때는 그에 따른 트레이드오프 (Tradeoff)를 알고 있어야 한다는 것입니다.
출력물은 여전히 전체 CSS 명세 (spec)입니다. 아무것도 제한되지 않습니다. 변하는 것은 당신의 값 (values)과 그 출력물 사이에 얼마나 많은 가드레일 (guardrails)을 설치하느냐 하는 것입니다. 어떤 프로젝트는 모든 컴포넌트에 걸쳐 엄격한 토큰 강제 (token enforcement)가 필요할 수 있습니다. 또 다른 프로젝트는 중요한 레이아웃 (layout) 내의 단일한 타입 지정 측정값 (typed measurement)이 필요할 수도 있습니다. 핵심은 프레임워크 (framework)가 아니라, 당신이 제약 수준 (constraint level)을 선택한다는 것입니다.
CSS는 브라우저 내에서는 조용히 실패 (fail silently)해야 합니다. 그것은 하나의 기능 (feature)입니다. 하지만 빌드 (build) 단계에서는 시끄럽게 실패 (fail loudly)해야 합니다. 그것이 현재 결여된 부분입니다. 이 논증에 대한 더 긴 버전을 원하신다면 진정한 CSS 프레임워크는 어떤 모습일지에 대해 제가 작성한 글을 읽어보시기 바랍니다.
References
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기