원문을 번역한 글이다. 마침 전 글에서 어그로가 어느정도 먹힌 것 같아서 어그로에 강황도 넣고 여러 향신료 쳐넣기 위해 향신료 하나 가져왔다. 내좆대로 번역했으니 꼬우면 직접 원문 읽어라.
Tailwind는 세상에서 제일 개쓰레기야. CSS와 최신 웹 개발의 모든 거지같은 점을 하나로 뭉쳐놓은, 뒤로 한참 개구린 라이브러리지.
현대 웹을 떠받치는 기술 중에서 CSS만큼 근본적인 변화가 적은 것도 없거든. Flexbox, Grid, 그리고 컨테이너 쿼리 같은 멋진 것들이 생겨서 반응형 스타일을 빠르게 짜는 건 이제 익숙해질 때도 됐잖아. 테이블로 레이아웃 짜던 시절이나 "CSS로 수직 정렬하는 법"을 매주 구글하던 때랑은 완전 딴판이지. CSS 자체의 문법은 별로 안 변했어. 왜? 인라인 스타일과 스타일시트, 두 가지 방식이 잘만 작동하니까.
인라인 스타일은 특정 요소에 빠르게 스타일을 입히는 데 와따야(역주: 내 틀니). 옛날엔 사람들이 이걸 남용해서 코드가 깔끔하지 않다고 까였지만, 솔직히 인라인 스타일 자체는 잘못이 하나도 없어. 문제라면 HTML 속성에 스타일을 쑤셔 넣는 게 좀 그지같이 보인다는 거 정도. 규칙이 많아지면 에디터에서 화면 밖으로 튀어나가고, 정리나 분석도 힘들지. 근데 좀 아는 개발자라면 이런 식으로 인라인 스타일을 자주 쓰진 않을 거야. (이제 스포일러, 오타쿠 용어로는 네타 가즈아.)
스타일시트는 훨씬 강력해. 한 번에 여러 요소에 규칙을 적용할 수 있고, 미디어 쿼리, 컨테이너 쿼리, 가상 요소 같은 고급 기능도 지원하지. 초기 스타일시트의 단점은 대부분 고쳐졌어. 물론 항상 깔끔한 방법은 아니었지만. SCSS 변수는 기본 CSS로 컴파일되니까 지원도 잘 됐고, CSS 변수보다 예뻤지. 하지만 CSS가 점점 나아지면서 SCSS는 힘을 잃었어.
CSS가 완벽하냐? 그건 아니잖아. 자바스크립트의 ==
연산자처럼, 예전에도 그지같고 지금도 그지같은 거 다들 알잖아. CSS는 선택자 우선순위로 스타일을 덮어쓰는데, 이게 디버깅할 힘들지. 결국 개발자들이 !important
태그를 남발하며 문제를 미루지. 더 짜증나는 건, 우선순위가 같을 때 스타일시트에서 나중에 정의된 게 이긴다는 거야. 예를 들어 보자.
<style>
.one {
text: red
}
.two {
text: blue
}
</style>
<div class="two one"></div>
이 경우 .two
가 나중에 정의됐으니 .two
스타일이 적용돼. 근데 .one
이 클래스 순서상 뒤에 있잖아? 멀티 시트랑 번들러 쓰는 현대 웹에선 이런 게 바로 혼돈 파괴 망가.
게다가 CSS가 자바스크립트와 별개 언어라는 건 양날의 검이라고 봐. 브라우저가 CSS를 병렬로 빠르게 처리할 수 있어서 좋지만, 현대 앱은 자바스크립트와 CSS가 연동돼야 해. CSS는 범용 언어가 아니니까 변수 공유하려면 코드 중복이 생기지.
이 문제들은 현대적인 도구들로 한층 수월해졌어, 물론 단점도 있겠지. 한국인들의 최애 프레임워크 styled-components 같은 도구는 자바스크립트와 CSS를 깔끔하게 합쳤지. 자바스크립트의 모든 기능을 CSS에서 쓸 수 있어. 단점? 스타일이 자바스크립트로 컴파일돼서 졸라게 느리고 단일 스레드에서 빡돌지. vanilla-extract는 JS로 작성한 스타일을 CSS 파일로 컴파일하면서 타입도 지원해. React 같은 도구는 인라인 스타일을 객체로 만들어서 린터, 타입 시스템, 포매터와 잘 맞게 했지. 스타일 파일로 컴파일하니 인상적이고.
Tailwind는 이 모든 문제를 개좆으로 만들면서 최신 웹 개발의 기본 스타일링 솔루션으로 튀어나왔어. CSS의 주요 패러다임을 Tailwind가 어떻게 따라하냐? 인라인 스타일은 단일 요소에 간단하고 명시적으로 style="background: red; color: blue;"
같은 스타일을 적용하지. 클래스는 여러 요소에 스타일을 간단히 적용하는데, 클래스 이름을 잘못 쓰면(class="todo-item"
) 디버깅 지옥에 빠지고 말아.
Tailwind는 클래스를 통한 인라인 스타일만 제공해. 클래스는 키-값 쌍이 아니라 공백으로 구분된 문자열이야. 그래서 class="bg-red txt-blue"
같은 식이지. 키와 값이 그냥 문자열로 뭉쳐져 있어서 읽기도 쓰기도 불편해. 문자열 잘못 쓰면 에디터가 경고도 안 해줘. 브라우저에서야 알지. txt-blue
가 틀렸고 text-blue
가 맞다는 거, 눈치챘어? 아니? Tailwind 쓰다 보면 매일 이런 실수 할 거야. 왜? Tailwind의 명명 규칙이 일관성 없거든. 맞다는 걸 눈치챘나? 아니면 매일 이런 실수를 반복할 건가? Tailwind는 이름 짓기에서 일관성이 없어서 이런 일이 다반사다.
스타일시트처럼 여러 요소에 규칙 세트를 적용하려면 Tailwind는 뭐를 주나?
아무것도 안 줘. 공식 권장사항은 상수 같은 기본 코딩 원칙을 무시하고 클래스 이름을 복붙하라는 거야. 그리고 클래스 수정할 땐 멀티 커서 편집을 쓰래. 뭔 개소리야?
물론 Tailwind의 지침을 무시하고 상수나 컴포넌트를 만들 수 있지만, 문자열로만 다루니까 한층 그지같네. CSS 규칙 10개만 있는 요소도 에디터에서 줄바꿈돼서 읽기 힘들고 확장도 어렵다고!
const sharedClass = `mx-auto max-w-md overflow-hidden rounded-xl bg-white shadow-md text-black border border-black border-solid`;
return <div className={clsx(sharedClass, {'display-none': !visible})}></div>
이거랑,
const sharedInline = {
marginLeft: 'auto',
marginRight: 'auto',
maxWidth: MEDIUM_WIDTH,
overflow: 'hidden',
borderRadius: EXTRA_LARGE_ROUNDED,
background: 'white',
boxShadow: MEDIUM_BOX_SHADOW,
color: 'black',
border: '1px solid black',
};
return <div styles={{...sharedInline, display: visible ? 'block' : 'none'}}></div>
비교해봐. 뭐가 더 좆같은지.
Tailwind는 CSS의 고질적인 문제에 해결책 따위 존재하지 않아. Tailwind 클래스는 번들러가 동적으로 생성해서 우선순위 충돌을 예측하기 더 어려워. 예를 들어,
<div className={clsx('text-red bg-blue', {
'text-blue bg-red': currentPage === activePage
})}></div>
이건 간단해 보이지? 비활성일 땐 빨강, 활성일 땐 파랑, 맞을까? 아니야. 생성된 스타일시트에서 어떤 클래스가 먼저 오는지 예측 불가야. Tailwind의 해결책? 또 기본 코딩 원칙 무시하고 비즈니스 로직을 복붙하래...
<div className={clsx('text-sm', {
'text-red bg-blue': currentPage !== activePage,
'text-blue bg-red': currentPage === activePage
})}></div>
결국 Tailwind 개발자들은 옛 CSS 개발자들처럼 !
접미사로 !important
를 남발하게 되는거지.
그렇지. CSS 문제에 해결책은커녕 더 악화시키면서, Tailwind는 플러그인과 번들러 없인 동작도 안 해. 그냥 웹 페이지에 드롭인 못해. 번들러는 단순히 파일을 뒤져서 Tailwind 클래스처럼 보이는 걸 찾아 CSS 규칙으로 만드는 기본 작업을 하는 거란다.
Tailwind의 장점으로 내세우는 게 번들 크기 최적화래. 근데 중대형 코드베이스에선 이 장점이 있는건지 모르겠어. CSS 양이 걱정될 정도로 코드베이스가 커지면, Tailwind는 모든 가능한 CSS 규칙에 클래스 하나씩 만들어내. 그래도 CSS는 병렬 로드라 빠르니까 큰 문제는 아닐 테지.
진짜 문제는 자바스크립트 번들 크기야. Tailwind는 거의 모든 요소에 여러 클래스를 붙이라고 해. 그 긴 문자열들이 느리고 단일 스레드인 자바스크립트 번들 크기를 늘려버리지. 간단한 예를 볼까?
.todo-item {
color: var(--warning-color);
background: blue;
}
<div className='todo'>1</div>
<div className='todo'>2</div>
<div className='todo'>3</div>
캬아, 근데 Tailwind로는...
.text-warning {
color: #ffcc00;
}
.bg-blue {
background: blue;
}
<div className='text-warning bg-blue'>1</div>
<div className='text-warning bg-blue'>2</div>
<div className='text-warning bg-blue'>3</div>
이 흔한 예에서 Tailwind는 CSS 번들도 더 크고, 자바스크립트 번들도 더 커져! CSS에서 동일 규칙이 여러 선택자에 반복돼서 CSS가 더 커질 거라 쳐도, CSS 번들 크기는 자바스크립트보다 훨씬 덜 중요해. React 프로젝트에서 Tailwind는 className에 CSS를 쓰니까 text-warning 같은 문자열이 JSX에 수백 번 반복돼. 이게 자바스크립트 번들 크기를 늘리고, 앱 전체가 거북이 씹창 되는거야.
Tailwind는 개발자의 정신적 부담을 줄여주긴 커녕 더 늘려. CSS 대신 Tailwind의 추상화를 배워야 해. background-color가 배경색을 설정한다는 건 알지만, bg- 접두사가 배경색을 뜻한다는 것도 알아야지. 미디어 쿼리, 컨테이너 쿼리 같은 CSS 기능은 CSS 방식과 Tailwind 방식, 두 번 배워야 하네?
Tailwind는 타입 안전성도 없고, 에디터 확장 외엔 개발자 도구도 거의 없어. 클래스 위에 커서 올려서 align-items: center인지 확인해야 해. 호버가 안 뜨면 오타거나 VSCode 확장이 죽은 거야. 그럼 "tailwind flexbox align items center"를 구글해야지. align-items-center도 아니고 align-center도 아니야, items-center야. Tailwind의 모든 문제는 이렇게 해결해야 한다고.
Tailwind가 메이저 버전 업그레이드하면 클래스 이름이 바뀌기도 해. 그럼 Tailwind가 제공하는 코드모드로 수천 개의 변경을 일일이 검토해야지. 이건 Tailwind가 만든 문제야. 다른 라이브러리는 타입 시스템, 경고, 명확한 에러로 API 변경을 쉽게 하지만, Tailwind는 문자열 파싱에 기반을 둬서 그럴 수가 없겠네.
Tailwind의 유일한 장점은 CSS의 "스타일시트 전용" 기능을 인라인으로 쉽게 쓸 수 있다는 거야. 예를 들어 큰 화면에서만 적용되는 규칙은 lg: 접두사로 간단히 설정 가능해. 원래 CSS론 귀찮은 작업이지. 근데 다른 CSS-in-JS 도구도 비슷한 기능을 제공해. 게다가 그건 확장성도 좋아. 큰 화면에서 규칙 다섯 개가 달라지면 lg:를 다섯 번 써야 하고, 하나하나 호버해서 확인해야 해. vanilla-extract라면 타입 체크된 미디어 쿼리 하나에 모든 규칙을 넣으면 돼.
Tailwind로 안 되는 건 결국 plain CSS나 자바스크립트 인라인 스타일로 돌아가야 해. 코드베이스를 이해하는 정신적 부담이 또 늘어나는 거지. Tailwind 클래스인지 plain CSS 클래스인지 구분하고, 스타일시트에서 그 정의를 찾아야 한다니...
이게 내가 말한 "최악의 도구"라는 뜻이야.
언어엔 두 종류가 있어. 사람들이 불평하는 거랑 아무도 안 쓰는 거.
Tailwind 성공의 가장 큰 요인은 코드베이스 전역 스타일 상수(색상, 마진 크기, 폰트, 보더 반경 등)를 설정하는 config 파일을 강제한다는 거야. Tailwind에선 이 상수를 안 쓰고 스타일을 쓰는 게 불편해. 이건 진짜 장점이야. 대형 코드베이스에 여러 프론트엔드 개발자가 있을 때 필요한 건 이런 엄격한 전역 상수라고.
Tailwind는 이 점에서 성공했고, 다른 라이브러리는 여기서 고전했지. vanilla-extract는 진짜 훌륭하지만, 새 코드베이스에 설정할 때 내가 직접 상수 구조를 만들어야 해. 난 이게 좋아. 자바스크립트로 색상을 반전하거나 틴트하는 식으로 상수를 만들 수 있으니까. 하지만 이 구조를 잘 만들고 지키려면 선견지명과 규율이 필요해. 문서도 써야 하고, 모든 개발자가 그걸 읽고 이해해야지.
Tailwind는 단일 마법의 config 파일로 모든 상수를 전역적으로 제공해. 상수 구조에 대한 질문은 "Tailwind가 그렇게 작동해"로 끝. prettier가 코드 포매팅 결정을 대신해준 것처럼, Tailwind는 상수 구조에 대한 논쟁을 없애줘. 이건 좋아. 더 이상 JS를 수동으로 포매팅 안 하듯이, Tailwind 덕에 일회성 색상도 거의 안 봐.
대부분의 회사가 Tailwind를 채택하는 건 표준과 일관된 색상 부족을 해결하는 즉석 솔루션이기 때문이야. Tailwind보다 나은 라이브러리가 있다고 생각해. 파일 크기도 작고 일관성도 더 좋아. 하지만 이런 라이브러리가 성공하려면 더 권위적이어야 해. vanilla-extract가 내 마음대로 하게 해주는 건 좋지만, 기본 구조를 제공하면서 필요하면 바꿀 수 있게 하면 채택이 늘겠네.
마지막으로, Tailwind는 LLM이나 바이브 코딩 도구가 기본으로 내뱉는 스타일링이야. 이건 새롭고도 익숙한 현상이야. 과거엔 StackOverflow 첫 번째 답변이 jQuery로 문제를 해결하는 거였고.
하지만 지금은 코드베이스 설계의 많은 결정을 단일 도구가 한 번에 내리고 있어. LLM에 이런 결정을 맡긴 개발자들은 코드베이스가 바이브로 감당 안 될 정도로 커지면 진짜 개발자가 필요할 때까지 최악의 도구를 쓰게 되는거지.
난 vanilla-extract가 좋아. 타입 안전한 가벼운 추상화로 CSS-in-JS를 거의 완벽하게 제공하지. 최고는 아니지만 거의 모든 면에서 훌륭해. plain CSS로 앱을 짜는 것도 나쁘진 않아. 근데 어떤 스타일이 어디 적용되는지 추적하기 힘들기는 해. React에서 기본 상수로 인라인 스타일만 써서 꽤 큰 앱을 만든 적도 있는데, 미디어 쿼리가 없고 JS 번들 크기가 커지는 문제가 있어. 어쨌든 내 요점은 최고의 도구가 있다는 게 아니야. Tailwind가 최악이라는 거지.
tailwindcss는 제쳐두고, 개인적으로 마지막 추천 라이브러리에 동의하는 게,
styled-components와 emotion은 번들러를 심하게 타고, 결정적으로 내가 안쓰게 만드는 요인이, 너무 스크립트에 의존적이다.
하지만 원글러가 추천한 vanilla-extract 는 번들러를 타는 점에서는 동일하지만, 출력 시 css로 출력한다는 점에서 다르다.
대부분 React 개발자들이니 이게 뭔 문제냐 하겠지만, 나한텐 문제다. 범용성은 중대사항이다. Vue와 Svelte도 혜택을 누려야 한다.
바닐라 익스트렉 보아라. 쌩 HTML에서도 이렇게 활용이 가능하다.
import { container } from './styles.css.ts';
document.write(`
<section class="${container}">
...
</section>
`);
당연하겠지만, 한국인이 좋아하는 CSS in JS 답게 이런 반론이 들어올 것이다.
그럼 동적 스타일은 어쩔 거냐? 대안이 있냐?
있다. 총 3가지 방법이 있다.
:hover
등의 표준 CSS내가 본 제일 최악의 케이스가, :hover
가상 선택자로 가능한 일을 동적 스타일링으로 해결하려는 모습이다.
여러분은 최소한 CSS가 제공하는 건 제발 쓰기 바란다.
예시 코드 쓸 것도 없이 다들 쓸 줄 안다 가정하고 예시 코드는 귀찮아서 패스하겠다.
어려울 것 없이 그냥 스크립트가 주는 혜택을 활용할 수 있잖느냐.
import { pinkButton, blueButton } from './button.css.ts';
const ButtonComponent = ({ variant = 'pink', children }) => {
return <button className={variant === 'pink' ? pinkButton : blueButton}>{children}</button>;
}
아니면 더 쉽게 styleVariants
함수를 활용하면 된다.
import { styleVariants } from '@vanilla-extract/css';
export const background = styleVariants({
primary: { background: 'blue' },
secondary: { background: 'aqua' }
});
import { background } from './styles.css.ts';
const Section = ({ variant, children }) => (
<section className={background[variant]}>{children}</section>
);
세세한 동적 스타일을 원한다면 여기서부터는 뭐 어쩔 수 없이, 바닐라 익스트랙이 제공하는 패키지를 별도로 설치하여 쓰면 된다.
npm install @vanilla-extract/dynamic
그리고 CSS 변수 기능을 활용한 스타일을 정의하여,
import { createVar, style } from '@vanilla-extract/css';
export const brandColor = createVar();
export const textColor = createVar();
export const container = style({
background: brandColor,
color: textColor
});
style 에다가 변수 재정의하도록 유도하면 된다.
import { assignInlineVars } from '@vanilla-extract/dynamic';
import {
container,
brandColor,
textColor
} from './styles.css.ts';
const MyComponent = ({ tone = '', children }) => (
<section
className={container}
style={assignInlineVars({
[brandColor]: 'pink',
[textColor]: tone === 'critical' ? 'red' : null
})}
>
{children}
</section>
);
그럼 난 이미 개발 표준에 tailwind로 이미 판 깔아버려가지고 그냥 그거 쓰러 간다. 그럼 이만!
끗.
저는 원래 CSS/SCSS를 주로 사용했기 때문에, 처음엔 Tailwind를 지양하는 입장이었습니다.
어차피 CSS도 컴파일과 압축 과정을 거치니 굳이 유틸리티 클래스를 남발할 필요가 있을까 싶었죠.
하지만 최근에는 Tailwind도 컴파일을 전제로 사용되고 있고, 다양한 UI 라이브러리와의 궁합도 좋아서 생각이 달라졌습니다.
제가 주로 사용하는 HTML, js, Alpine.js, Lit, Web Components, svelte 환경에서는 오히려 Tailwind가 더 직관적으로 다가와 지금은 주력으로 사용하고 있습니다.
group, has, peer, @container, @theme, @apply 같은 기능들을 활용해 상황에 맞게 대응하며 사용하다 보니, 이런 방식도 꽤 편리하다는 걸 느끼고 있습니다.
스타일이 점점 늘어나더라도, 공통 스타일을 정의하기보다는 상황에 따라 유연하게 대응한다는 관점으로 작업하고 있습니다.
어차피 css 패턴은 비슷하게 작성 하는 것 같아서요
참고로 저는 CSS-in-JS 방식은 개인적으로 선호하지 않습니다.
앞으로 언제까지 Tailwind를 사용할지는 모르겠지만, 당분간은 계속 활용할 것 같네요.
Flexbox, Grid, 그리고 컨테이너 쿼리 같은 멋진 것들이 생겨서 반응형 스타일을 빠르게 짜는 건 이제 익숙해질 때도 됐잖아. https://selfreliantenergycompany.com
좋은 글이군요. 그런 고민에서 개발한 라이브러리, 여기 있습니다.
https://github.com/danpacho/tailwindest