
Tailwind CSS: 유틸리티 퍼스트 CSS
클래스 이름 짓기 지치셨나요? HTML 안에 CSS를 직접 쓰는 기괴한 방식이 왜 전 세계 프론트엔드 표준이 되었는지 파헤쳐봤습니다.

클래스 이름 짓기 지치셨나요? HTML 안에 CSS를 직접 쓰는 기괴한 방식이 왜 전 세계 프론트엔드 표준이 되었는지 파헤쳐봤습니다.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

처음 테일윈드를 봤을 때 든 생각입니다. 커뮤니티 선배가 코드를 보여주는데:
<div class="flex items-center justify-between px-6 py-4 bg-white rounded-lg shadow-md">
<h2 class="text-2xl font-bold text-gray-800">제목</h2>
<button class="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600">
클릭
</button>
</div>
"아니 이게 뭐야... HTML이 왜 이렇게 지저분해?" 컴공도 아니고 디자인 전공자도 아닌 제가 봐도 이건 아닌 것 같았습니다. 그냥 인라인 스타일에 이름만 붙여놓은 거 아닌가요?
선배는 웃으면서 "한번 써봐, 못 돌아간다"고 했습니다.
그때 나는 이해하지 못했습니다. 왜 멀쩡한 CSS를 버리고 이런 걸 쓰는지.
제가 처음 웹을 배울 때는 "관심사 분리(Separation of Concerns)"가 정답이었습니다. HTML은 구조, CSS는 스타일, JavaScript는 동작. 깔끔하게 분리하는 게 올바른 방식이라고 배웠죠.
그래서 이렇게 코드를 짰습니다:
<!-- HTML -->
<div class="card">
<h2 class="card__title">제목</h2>
<button class="card__button">클릭</button>
</div>
/* CSS */
.card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.card__title {
font-size: 1.5rem;
font-weight: bold;
color: #1f2937;
}
.card__button {
padding: 0.5rem 1rem;
color: white;
background-color: #3b82f6;
border-radius: 0.25rem;
}
.card__button:hover {
background-color: #2563eb;
}
BEM 방식으로 클래스 이름 짓고, CSS 파일 따로 만들고. 얼핏 보면 깔끔합니다.
하지만 프로젝트가 커지면서 문제가 보이기 시작했습니다:
1. 클래스 이름 짓기의 지옥.card-container, .card-wrapper, .card-inner, .card-content, .card-header-section... 이게 뭐가 다른지 나도 모르겠습니다. 나중에 코드 보면 "이거 내가 왜 이렇게 지었지?"라는 생각만 듭니다.
버튼 색깔 하나 바꾸려면 HTML에서 클래스 이름 확인하고, CSS 파일 열어서 찾고, 수정하고, 다시 HTML로 돌아와서 확인하고... 마치 책 두 권을 동시에 펼쳐놓고 읽는 기분입니다.
3. CSS 파일이 무한정 커짐새로운 컴포넌트 만들 때마다 CSS가 계속 추가됩니다. 그리고 이전에 만든 CSS를 재사용할 수 있는지 찾아보지만, 결국 "비슷하지만 조금 달라서" 새로 만듭니다. 그러다 보면 CSS 파일이 3000줄을 넘어가고, 어느 부분이 어디에 쓰이는지 아무도 모릅니다.
4. 똑같은 스타일인데 이름만 다른 경우.margin-top-small { margin-top: 8px; }
.mt-2 { margin-top: 8px; }
.header-spacing { margin-top: 8px; }
결국 같은 스타일을 세 번 정의합니다. 누군가는 .margin-top-small을 썼고, 다른 사람은 .mt-2를 만들었고, 또 다른 사람은 .header-spacing을 만들었습니다.
이게 "관심사 분리"의 함정이었습니다. 파일은 분리됐지만, 정작 개발자의 머릿속은 분리되지 않았습니다. CSS와 HTML을 동시에 생각해야 했으니까요.
프로젝트를 3개월쯤 진행했을 때, CSS 파일이 너무 복잡해져서 리팩토링하기로 했습니다. 그때 선배가 "그냥 테일윈드 써보자"고 제안했습니다.
처음에는 정말 거부감이 심했습니다. 클래스 이름이 너무 길어서 HTML이 지저분해 보였으니까요.
하지만 하루 써보니까 충격을 받았습니다.
CSS 파일을 하나도 안 만들었는데 모든 스타일링이 끝났습니다.HTML만 보면서 디자인을 완성했습니다. 파일을 왔다갔다할 필요가 없었습니다. 클래스 이름을 고민할 필요도 없었습니다. 그냥 flex, items-center, px-4 이렇게 쓰면 됐습니다.
마치 레고 블록을 조립하는 느낌이었습니다. 미리 만들어진 조각들을 조합해서 원하는 모양을 만드는 거죠.
그때 이해했습니다. 테일윈드는 "인라인 스타일의 편리함"과 "CSS의 재사용성"을 결합한 거였습니다.
테일윈드의 핵심은 Utility-First 철학입니다. 의미 있는 이름 대신, 기능 단위로 클래스를 제공하는 겁니다.
전통적인 CSS는 이렇게 생각합니다:
.card 클래스를 만들자".button 클래스를 만들자"테일윈드는 이렇게 생각합니다:
.flex를 쓰자".p-4를 쓰자".bg-blue-500을 쓰자"마치 원자(atom)처럼 작은 단위들을 조합해서 분자(molecule)를 만드는 방식입니다.
전통적인 CSS에서 반응형 디자인 만들기:
.grid-container {
display: block;
}
@media (min-width: 640px) {
.grid-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.grid-container {
grid-template-columns: repeat(3, 1fr);
}
}
테일윈드에서:
<div class="block sm:grid sm:grid-cols-2 lg:grid-cols-3">
<!-- 내용 -->
</div>
한 줄입니다. 미디어 쿼리를 직접 쓸 필요가 없습니다. sm:, md:, lg: 접두사만 붙이면 됩니다.
처음에는 이것도 "외워야 하는 게 많네?"라고 생각했는데, 사용하다 보니 패턴이 보입니다:
sm:md:lg:xl:이제는 미디어 쿼리 숫자를 외울 필요가 없습니다.
다크모드 구현할 때 보통 이렇게 합니다:
.card {
background-color: white;
color: black;
}
@media (prefers-color-scheme: dark) {
.card {
background-color: #1f2937;
color: white;
}
}
테일윈드에서:
<div class="bg-white dark:bg-gray-800 text-black dark:text-white">
<!-- 내용 -->
</div>
dark: 접두사만 붙이면 됩니다. 시스템 다크모드를 자동으로 감지합니다.
수동으로 토글하고 싶으면 tailwind.config.js에서:
module.exports = {
darkMode: 'class', // 'media' 대신 'class' 사용
// ...
}
그러면 <html class="dark"> 클래스를 붙였을 때만 다크모드가 적용됩니다.
이론보다 코드를 보는 게 이해가 빠릅니다. 반응형 카드 컴포넌트를 만들어봅시다.
요구사항:<div class="card-grid">
<div class="card">
<img src="thumbnail.jpg" class="card-image" />
<h3 class="card-title">제목</h3>
<p class="card-description">설명</p>
</div>
</div>
.card-grid {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.card-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.card-grid { grid-template-columns: repeat(3, 1fr); }
}
.card {
background: white;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-title {
padding: 1rem;
font-size: 1.25rem;
font-weight: 600;
}
.card-description {
padding: 0 1rem 1rem;
color: #6b7280;
}
@media (prefers-color-scheme: dark) {
.card {
background: #1f2937;
}
.card-title {
color: white;
}
.card-description {
color: #9ca3af;
}
}
테일윈드:
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-md hover:shadow-lg hover:-translate-y-1 transition-all">
<img src="thumbnail.jpg" class="w-full h-48 object-cover" />
<h3 class="px-4 pt-4 text-xl font-semibold text-gray-900 dark:text-white">
제목
</h3>
<p class="px-4 pb-4 text-gray-600 dark:text-gray-300">
설명
</p>
</div>
</div>
CSS 파일 30줄 vs HTML 안에서 완결. 어느 쪽이 빠를까요?
나는 테일윈드가 훨씬 빠르다고 느꼈습니다. CSS 파일 왔다갔다할 필요 없이 HTML만 보면서 작업하니까요.
클래스가 너무 길어지면 컴포넌트로 추출하면 됩니다.
React 예제:function Card({ title, description, image }) {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-md hover:shadow-lg hover:-translate-y-1 transition-all">
<img src={image} className="w-full h-48 object-cover" />
<h3 className="px-4 pt-4 text-xl font-semibold text-gray-900 dark:text-white">
{title}
</h3>
<p className="px-4 pb-4 text-gray-600 dark:text-gray-300">
{description}
</p>
</div>
);
}
// 사용
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Card title="제목 1" description="설명 1" image="img1.jpg" />
<Card title="제목 2" description="설명 2" image="img2.jpg" />
</div>
중복되는 긴 클래스는 컴포넌트로 빼면 됩니다. 그러면 재사용도 쉽고, HTML도 깔끔해집니다.
컴포넌트를 만들기 애매할 때는 @apply를 쓸 수 있습니다:
/* styles.css */
@layer components {
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors;
}
}
<button class="btn-primary">클릭</button>
하지만 테일윈드 공식 문서는 @apply를 최소한으로 쓰라고 권장합니다. 왜냐하면 테일윈드의 장점을 포기하는 거니까요.
나는 정말 필요할 때만 씁니다. 같은 스타일을 10번 이상 반복할 때만요.
예전 테일윈드는 모든 유틸리티 클래스를 미리 생성했습니다. 그래서 개발 중에 CSS 파일이 3-4MB까지 커졌죠.
Tailwind 3.0부터는 JIT(Just-In-Time) 모드가 기본입니다. 필요한 클래스만 즉시 생성합니다.
<!-- 이런 걸 쓰면 -->
<div class="top-[117px]">
테일윈드가 즉시 .top-\[117px\] { top: 117px; } 클래스를 생성합니다.
예전에는 미리 정의된 값(top-0, top-1, top-2 등)만 쓸 수 있었는데, 이제는 임의의 값을 쓸 수 있습니다:
<div class="bg-[#1da1f2]"> <!-- 트위터 블루 -->
<div class="w-[347px]"> <!-- 디자이너가 준 정확한 너비 -->
<div class="grid-cols-[1fr_2fr_1fr]"> <!-- 커스텀 그리드 -->
이게 게임체인저였습니다. 디자이너가 "이 색은 정확히 #1da1f2여야 합니다"라고 하면, config 파일 수정 없이 바로 쓸 수 있습니다.
프로젝트마다 디자인 시스템이 다릅니다. 테일윈드는 설정 파일로 확장할 수 있습니다:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: '#5b21b6',
secondary: '#ec4899',
},
spacing: {
'128': '32rem',
'144': '36rem',
},
fontFamily: {
sans: ['Pretendard', 'sans-serif'],
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}
그러면 이렇게 쓸 수 있습니다:
<div class="text-primary bg-secondary h-128 font-sans">
회사 디자인 시스템을 config에 정의해놓으면, 팀원들이 일관된 스타일을 사용하게 됩니다. "margin: 13px" 같은 이상한 값을 못 쓰게 강제하는 거죠.
개발 중에는 모든 유틸리티 클래스를 생성합니다. 하지만 프로덕션에서는 실제로 사용한 클래스만 남겨야 합니다.
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./public/index.html',
],
// ...
}
빌드할 때 테일윈드가 이 파일들을 스캔해서, 실제로 사용된 클래스만 최종 CSS에 포함시킵니다.
결과: 개발 중 3MB → 프로덕션 10KB
이게 테일윈드의 마법입니다. 무한한 유틸리티를 제공하지만, 최종 번들은 작습니다.
팀에서 논쟁이 있었습니다. "테일윈드 vs styled-components 뭐가 나아?"
styled-components 진영:나는 둘 다 써봤는데, 프로젝트 성격에 따라 다르다고 느꼈습니다:
하지만 요즘은 대부분 테일윈드를 선택합니다. 왜냐하면 성능이 더 좋고, 번들 크기가 작고, 러닝커브가 낮으니까요.
테일윈드는 플러그인으로 확장할 수 있습니다:
공식 플러그인:@tailwindcss/forms: 폼 스타일 리셋@tailwindcss/typography: 마크다운 prose 스타일@tailwindcss/aspect-ratio: 종횡비 유틸리티@tailwindcss/line-clamp: 텍스트 줄 수 제한// tailwind.config.js
module.exports = {
plugins: [
require('@tailwindcss/typography'),
],
}
<!-- 마크다운 콘텐츠에 적용 -->
<article class="prose lg:prose-xl dark:prose-invert">
<h1>제목</h1>
<p>본문...</p>
</article>
prose 클래스 하나로 아름다운 타이포그래피가 완성됩니다.
처음 테일윈드를 봤을 때는 "이게 뭐야, 인라인 스타일이랑 뭐가 다르지?"라고 생각했습니다.
하지만 써보니까 알았습니다. 이건 단순히 CSS를 HTML에 쓰는 게 아니라, 디자인 시스템을 강제하면서도 생산성을 극대화하는 방법이었습니다.
클래스 이름 짓기의 고통에서 해방됐고, CSS 파일 왔다갔다하는 시간이 사라졌고, 팀원들과 일관된 스타일을 유지할 수 있게 됐습니다.
물론 단점도 있습니다. HTML이 지저분해 보이고, 처음에는 클래스 이름 외우는 게 어렵습니다.
하지만 한번 익숙해지면 돌아갈 수 없습니다. 마치 VS Code를 처음 쓸 때처럼요. 처음엔 설정이 복잡하지만, 익숙해지면 다른 에디터는 못 쓰게 되는 것처럼.
테일윈드는 이제 제 기본 스택입니다. 새 프로젝트 시작할 때 가장 먼저 설치하는 라이브러리가 됐습니다.
결국 이거였습니다: "관심사 분리"보다 "개발자 경험"이 더 중요하다.