핵심 개념
중복 관리와 재사용 가능한 추상화 만들기
Tailwind는 유틸리티 우선 방식의 작업 흐름을 권장합니다. 이 방식은 낮은 수준의 유틸리티 클래스만 사용하여 디자인을 구현합니다. 이는 조기 추상화와 그로 인한 문제점을 피할 수 있는 강력한 방법입니다.
하지만 프로젝트가 커지면, 여러 곳에서 동일한 디자인을 재현하기 위해 공통 유틸리티 조합을 반복적으로 사용하게 됩니다.
예를 들어, 아래 템플릿에서 각 아바타 이미지에 대한 유틸리티 클래스가 다섯 번 반복되는 것을 볼 수 있습니다.
<div>
<div class="flex items-center space-x-2 text-base">
<h4 class="font-semibold text-slate-900">Contributors</h4>
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700">204</span>
</div>
<div class="mt-3 flex -space-x-2 overflow-hidden">
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""/>
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""/>
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80" alt=""/>
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""/>
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="https://images.unsplash.com/photo-1517365830460-955ce3ccd263?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""/>
</div>
<div class="mt-3 text-sm font-medium">
<a href="#" class="text-blue-500">+ 198 others</a>
</div>
</div>
걱정하지 마세요! 이 가이드에서는 프로젝트에서 스타일을 재사용하는 다양한 전략과 각 전략을 사용해야 할 때의 모범 사례를 배우게 됩니다.
이와 같은 중복은 실제로 문제가 되지 않는 경우가 많습니다. 모든 코드가 한 곳에 모여 있거나, 배열을 순회하며 마크업을 한 번만 작성하기 때문입니다.
재사용해야 할 스타일이 단일 파일 내에서만 필요하다면, 멀티 커서 편집과 반복문을 사용하는 것이 중복을 관리하는 가장 간단한 방법입니다.
단일 파일 내에서 특정 요소 그룹에 중복이 발생한 경우, 가장 간단한 해결 방법은 멀티 커서 편집을 사용하여 각 요소의 클래스 목록을 한 번에 선택하고 편집하는 것입니다:
<nav class="flex justify-center space-x-4">
<a href="/dashboard" class="font-medium px-3 py-2 text-slate-700 rounded-lg hover:bg-slate-100 hover:text-slate-900">Home</a>
<a href="/team" class="font-medium px-3 py-2 text-slate-700 rounded-lg hover:bg-slate-100 hover:text-slate-900">Team</a>
<a href="/projects" class="font-medium px-3 py-2 text-slate-700 rounded-lg hover:bg-slate-100 hover:text-slate-900">Projects</a>
<a href="/reports" class="font-medium px-3 py-2 text-slate-700 rounded-lg hover:bg-slate-100 hover:text-slate-900">Reports</a>
</nav>
이 방법이 가장 효과적인 경우가 많다는 사실에 놀라실 수도 있습니다. 모든 중복된 클래스 목록을 동시에 빠르게 편집할 수 있다면, 추가적인 추상화를 도입할 필요가 없습니다.
템플릿에서 컴포넌트를 추출하거나 커스텀 클래스를 만들기 전에, 해당 요소가 실제로 여러 번 사용되는지 확인하세요.
렌더링된 페이지에서 여러 번 나타나는 디자인 요소는 실제로는 한 번만 작성되는 경우가 많습니다. 이는 실제 마크업이 반복문 안에서 렌더링되기 때문입니다.
예를 들어, 이 가이드의 시작 부분에 있는 중복된 아바타는 실제 프로젝트에서 거의 확실히 반복문 안에서 렌더링될 것입니다:
<div>
<div class="flex items-center space-x-2 text-base">
<h4 class="font-semibold text-slate-900">Contributors</h4>
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700">204</span>
</div>
<div class="mt-3 flex -space-x-2 overflow-hidden">
{#each contributors as user}
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" src="{user.avatarUrl}" alt="{user.handle}"/>
{/each}
</div>
<div class="mt-3 text-sm font-medium">
<a href="#" class="text-blue-500">+ 198 others</a>
</div>
</div>
원한다면 네비게이션 예제도 반복문이나 map
을 사용해 다시 작성할 수 있습니다:
<nav className="flex sm:justify-center space-x-4">
{[
['Home', '/dashboard'],
['Team', '/team'],
['Projects', '/projects'],
['Reports', '/reports'],
].map(([title, url]) => (
<a href={url} className="rounded-lg px-3 py-2 text-slate-700 font-medium hover:bg-slate-100 hover:text-slate-900">{title}</a>
))}
</nav>
이렇게 반복문 안에서 엘리먼트를 렌더링하면 실제 클래스 목록은 한 번만 작성되므로 중복 문제를 해결할 필요가 없습니다.
여러 파일에서 스타일을 재사용해야 한다면, React, Svelte, Vue 같은 프론트엔드 프레임워크를 사용 중이라면 컴포넌트를, Blade, ERB, Twig, Nunjucks 같은 템플릿 언어를 사용 중이라면 부분 템플릿을 만드는 것이 가장 좋은 전략입니다.
<template>
<div>
<img class="rounded" :src="img" :alt="imgAlt">
<div class="mt-2">
<div>
<div class="text-xs text-slate-600 uppercase font-bold tracking-wider">{{ eyebrow }}</div>
<div class="font-bold text-slate-700 leading-snug">
<a :href="url" class="hover:underline">{{ title }}</a>
</div>
<div class="mt-2 text-sm text-slate-600">{{ pricing }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['img', 'imgAlt', 'eyebrow', 'title', 'pricing', 'url']
}
</script>
이제 이 컴포넌트를 원하는 만큼 여러 곳에서 사용할 수 있습니다. 스타일은 단일 소스에서 관리되기 때문에 한 곳에서 쉽게 업데이트할 수 있습니다.
컴포넌트가 단일 HTML 엘리먼트가 아닌 경우, CSS만으로는 컴포넌트를 정의하는 데 필요한 정보를 충분히 담아낼 수 없습니다. 조금이라도 복잡한 구조라면 HTML 구조는 CSS만큼 중요합니다.
복잡한 컴포넌트를 CSS 클래스로 추출하려고 하지 마세요
You have a new message!
<!-- 커스텀 CSS를 사용하더라도 이 HTML 구조를 반복해야 합니다 -->
<div class="chat-notification">
<div class="chat-notification-logo-wrapper">
<img class="chat-notification-logo" src="/img/logo.svg" alt="ChitChat Logo">
</div>
<div class="chat-notification-content">
<div class="chat-notification-title">ChitChat</div>
<p class="chat-notification-message">You have a new message!</p>
</div>
</div>
<style>
.chat-notification { /* ... */ }
.chat-notification-logo-wrapper { /* ... */ }
.chat-notification-logo { /* ... */ }
.chat-notification-content { /* ... */ }
.chat-notification-title { /* ... */ }
.chat-notification-message { /* ... */ }
</style>
이렇게 컴포넌트 내의 각 엘리먼트에 클래스를 만들어도, 이 컴포넌트를 사용할 때마다 HTML을 반복해야 합니다. 물론 모든 인스턴스의 폰트 크기를 한 곳에서 업데이트할 수는 있지만, 제목을 링크로 바꿔야 한다면 어떻게 될까요?
컴포넌트와 템플릿 부분은 CSS만으로 추상화하는 것보다 이 문제를 훨씬 잘 해결합니다. 컴포넌트는 HTML과 스타일을 모두 캡슐화할 수 있기 때문입니다. 모든 인스턴스의 폰트 크기를 변경하는 것은 CSS만 사용할 때와 마찬가지로 쉽지만, 이제는 모든 제목을 한 곳에서 링크로 바꿀 수도 있습니다.
템플릿 부분 또는 JavaScript 컴포넌트를 만드세요
You have a new message!
function Notification({ imageUrl, imageAlt, title, message }) {
return (
<div className="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md flex items-center space-x-4">
<div className="shrink-0">
<img className="h-12 w-12" src={imageUrl.src} alt={imageAlt}>
</div>
<div>
<div className="text-xl font-medium text-black">{title}</div>
<p className="text-slate-500">{message}</p>
</div>
</div>
)
}
이렇게 컴포넌트와 템플릿 부분을 만들면, 스타일에 대한 단일 출처가 이미 있기 때문에 유틸리티 클래스 외에 다른 것을 사용할 이유가 없습니다.
ERB나 Twig 같은 전통적인 템플릿 언어를 사용한다면, 버튼처럼 작은 요소를 위해 템플릿 부분을 만드는 것이 btn
같은 간단한 CSS 클래스에 비해 과도하게 느껴질 수 있습니다.
더 복잡한 컴포넌트에는 적절한 템플릿 부분을 만드는 것이 권장되지만, 템플릿 부분이 과하다고 느껴질 때는 Tailwind의 @apply
지시어를 사용해 반복되는 유틸리티 패턴을 커스텀 CSS 클래스로 추출할 수 있습니다.
다음은 기존 유틸리티를 조합해 btn-primary
클래스를 만드는 예제입니다:
<!-- 커스텀 클래스 추출 전 -->
<button class="py-2 px-5 bg-violet-500 text-white font-semibold rounded-full shadow-md hover:bg-violet-700 focus:outline-none focus:ring focus:ring-violet-400 focus:ring-opacity-75">
Save changes
</button>
<!-- 커스텀 클래스 추출 후 -->
<button class="btn-primary">
Save changes
</button>
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn-primary {
@apply py-2 px-5 bg-violet-500 text-white font-semibold rounded-full shadow-md hover:bg-violet-700 focus:outline-none focus:ring focus:ring-violet-400 focus:ring-opacity-75;
}
}
@apply
와 @layer
에 대해 더 알아보려면 함수와 지시문 문서를 참고하세요.
무엇을 하든, 단지 “깔끔해 보이기 위해” @apply
를 사용하지 마세요. Tailwind 클래스로 가득 찬 HTML 템플릿이 보기 흉할 수는 있지만, 커스텀 CSS가 잔뜩 들어간 프로젝트에서 변경을 하는 것은 더 나쁩니다.
만약 모든 것에 @apply
를 사용하기 시작한다면, 여러분은 기본적으로 CSS를 다시 작성하는 것이고, Tailwind가 제공하는 워크플로우와 유지보수성의 장점을 모두 버리는 것입니다. 예를 들어:
@apply
를 사용할 거라면, 버튼이나 폼 컨트롤과 같이 매우 작고 재사용성이 높은 요소에만 사용하세요. 그리고 React와 같은 프레임워크를 사용하지 않는 경우에만 사용하는 것이 좋습니다. 컴포넌트가 더 나은 선택일 수 있습니다.