
프론트엔드 개발을 하다 보면, 반복되는 스타일을 어떻게 관리할지는 늘 따라다니는 고민입니다.
특히 Tailwind CSS를 사용할 때는 상황이 더 자주 발생합니다. 비슷한 디자인의 버튼이 여러 개 있는데, 매번
className으로 스타일을 직접 작성하고 있다면, 나중에 버튼 디자인을 한 번에 바꿔야 할 때 꽤 곤란해질 수밖에 없습니다.이 문제를 풀기 위해 보통은 두 가지 접근을 떠올리게 됩니다.
- 재사용 가능한 컴포넌트 만들기
- Custom Utility Class 만들기
이번에 새로운 프로젝트를 진행하면서, “Button 스타일을 어디까지 컴포넌트로 두고, 어디까지 CSS(Tailwind 유틸리티)로 처리할 것인가?”를 깊게 고민해볼 기회가 있었습니다.
그 과정에서 얻은 인사이트를 공유해보려고 합니다.
상황: Button 컴포넌트의 딜레마
프로젝트에서 여러 곳에 동일한 스타일의 Button이 필요했습니다. 처음에는 이렇게 컴포넌트로 만들어서 관리하려고 했습니다.
프로젝트 곳곳에서 동일한 스타일의 버튼이 필요했습니다.
그래서 처음에는 아래처럼 Button 컴포넌트를 하나 만들어서 관리하려고 했습니다.
중복된 요소를 추상화하는 가장 직관적인 방법은, 이렇게 공통 Button 컴포넌트를 만드는 것이었습니다. 컴포넌트를 만드는 일 자체는 어렵지 않고, 복잡하게 설계하지 않아도 되니 자연스럽게 이 선택을 하게 됐죠.
그런데 막상 프로젝트를 진행하다 보니, 버튼과 같은 디자인을 쓰는 링크(link) 도 필요해지는 상황이 생겼습니다.
이 지점부터
Button 컴포넌트 안에서 처리해야 할 고민들이 하나둘씩 늘어나기 시작했습니다.예를 들어, 링크에도 같은 스타일을 적용하고 싶다면,
variant 같은 속성을 받아서 variant로 버튼과 링크를 분기 처리하는 내부 로직을 만들게 됩니다.이렇게 버튼과 링크를 하나의 컴포넌트로 묶어 쓰기 위해
두 가지 props 타입을 정의하고,
variant로 분기해주는 구조를 만들었습니다.물론 실제 서비스에서 사용할 수준으로 컴포넌트를 만들려면
여기에 추가적인 동작(비활성화 상태, 로딩 상태, 아이콘 위치, 접근성 처리 등) 이 계속 붙을 것이고,
그만큼 컴포넌트의 복잡도는 더 가파르게 올라가게 됩니다.
그리고 여기서 고민이 끝나지 않습니다.
디자인 시스템에는 보통 primary, secondary, ghost, danger 같은 여러 종류의 버튼이 존재할 수 있습니다.
기본 구조는 같지만, 테마 색상만 다른 경우도 있지만, 조금 더 나아가면 hover 애니메이션, 전환 효과, 사이즈(large/small) 등이 제각각 달라지기도 합니다.
이 모든 경우를 하나의 Button 컴포넌트 안에서 다 처리하는 것이 불가능한 건 아닙니다.
원한다면 props를 계속 늘려가며 “만능 버튼”을 만들 수도 있겠죠.
하지만 그렇게 만든 뒤 결과물을 다시 마주했을 때,
이미 너무 비대해져 버린 컴포넌트를 보고 있는 제 자신을 발견하게 되었습니다.
그리고 이 지점에서, “이걸 정말 컴포넌트로 모두 관리하는 게 맞을까?”라는 질문을 하게 되었습니다.
일관성 vs 추상화
문제를 해결하기 위해서는 두 가지 선택지가 있었습니다:
선택지 1: Button 컴포넌트 유지
- 장점 : 타입 안정성, IDE 자동완성, 일관된 사용 방식
- 단점 :
button에만 사용 가능, 확장을 위해 props가 점점 늘어남
첫 번째는 지금까지 만들어 둔 Button 컴포넌트를 그대로 가져가는 방식입니다.
이미 앞에서는 컴포넌트가 비대해질 수 있다는 점을 이야기하긴 했지만,
그래도 컴포넌트로 관리하면 정해진 props 안에서 동작을 “제어”할 수 있다는 점에서 분명한 안정감이 있습니다.
사용하는 입장에서도 타입이 보장되고, IDE 자동완성 도움을 받으면서 안전하게 쓸 수 있죠.
다만 이 “제어 가능함”이 동시에 단점이 되기도 합니다.
케이스가 늘어날수록 이를 모두 담기 위해 props가 계속 추가되고, 결국은 “만능 Button 컴포넌트”가 되어버리면서 점점 더 다루기 어려워집니다.
선택지 2: Custom Utility Class로 변경
- 장점 : 유연성, 태그와 관계없이 일관된 스타일 적용 가능
- 단점 : 문자열 기반이라 타입 체크가 되지 않음
두 번째는 Custom Utility Class를 만들어 쓰는 방식입니다.
Tailwind CSS로 공통 스타일을 하나의 커스텀 클래스로 정의해 두고, 버튼이든 링크든, 어떤 태그든 간에
className에 클래스를 넣어 주기만 하면 동일한 스타일을 일관되게 적용할 수 있는 방식입니다.태그의 종류와 상관없이 스타일을 재사용할 수 있다는 점에서 훨씬 유연하지만,
반대로 스타일이 문자열로만 관리되기 때문에 잘못된 클래스명을 써도 컴파일 타임에 잡히지 않는다는 아쉬움이 남습니다.
이렇게 두 가지 방법 사이에서, “일관된 제어와 타입 안정성을 택할 것인가, 아니면 유연한 적용 방식과 단순함을 택할 것인가”가 이번 고민의 핵심이었습니다.
Custom Utility Class 선택
고민 끝에 저는 Custom Utility Class를 사용하는 방식을 선택했습니다.
그 이유는 크게 세 가지였습니다.
1. 더 높은 유연성
기존 테마에서 일부만 수정하거나, 상황에 따라 스타일을 조금씩 덧입혀야 하는 경우가 많습니다.
이때
button 컴포넌트 내부 구현을 이해하고, props 조합을 외워가며 사용하는 대신“버튼 스타일이 필요하면 button 클래스를 붙인다.”
라는 단 하나의 규칙만 알고 있으면 됩니다.
나머지는
w-full, w-120처럼 필요한 Tailwind 유틸리티를 바로 옆에 붙여서 조합하면 되기 때문에,
컴포넌트의 구조나 내부 로직을 전부 파악해야 하는 부담이 사라집니다.2. 특별한 로직이 없다면, 굳이 컴포넌트일 필요가 없다
버튼이 “버튼만이 할 수 있는 특별한 일”을 한다면, 그때는 컴포넌트로 만드는 게 맞다고 생각합니다.
예를 들어:
- 버튼을 클릭할 때마다 공통 로그를 쌓아야 한다
- 공통한 Disabled 처리나 접근성 로직(ARIA 속성 등)이 항상 같이 들어가야 한다
이런 경우에는 매번 onClick에 같은 코드를 넣는 것보다,
<LoggingButton /> 같은 컴포넌트로 추상화해서 관리하는 편이 훨씬 낫습니다.하지만 이번 사례처럼, “특별한 동작 없이 스타일만 공유하는 요소”라면 이야기가 달라집니다.
이럴 땐 굳이 컴포넌트로 감싸기보다는,
공통 스타일은 커스텀 클래스에 맡기고, 동작은 각자 구현부에서 관리하는 편이 더 단순하다고 느꼈습니다.
3. 다양한 요소에 자연스럽게 적용 가능
앞에서 이야기했듯이, 같은 스타일을 버튼뿐 아니라 링크에도 쓰고 싶을 수 있습니다.
이처럼 “역할은 다르지만, 모양은 같아야 하는” HTML 요소들이 프로젝트 안에는 생각보다 자주 등장합니다.
컴포넌트로만 이 문제를 풀려고 하면:
<Button as="a" />같은 props를 추가하거나
- variant를 더 늘리거나
- 내부에서 태그를 분기하는 로직이 점점 복잡해지기 쉽습니다.
반면, Custom Utility Class를 쓰면 태그 종류와 상관없이
className="button" 한 줄로 해결됩니다.
저는 이 단순함과 일관성이 이번 선택의 중요한 기준이었습니다.구현
최종적으로는 공통 스타일은 한 번만 정의하고, 테마별로 확장하는 구조로 정리했습니다.
공통으로 항상 들어가는 테마 요소는
button-base에만 정의해 두고, button, line-button처럼 테마별 스타일은 button-base를 상속(@apply)한 뒤 각각 필요한 부분만 덧붙이는 방식으로 구성했습니다.이렇게 해두면:
- 버튼 계열 컴포넌트의 공통 스타일은 한 곳에서만 관리할 수 있고
- 새 테마가 필요할 때도
@utility만 하나 추가하면 되니 확장성도 확보할 수 있습니다.
정리
스타일만 추상화한다면 → Custom Utility Class
- 가볍고 유연하게 사용할 수 있음
- 프로젝트 전체 디자인 시스템(톤&매너, 토큰 등) 관리에 유리함
- 버튼, 링크, div 등 다양한 HTML 요소에 공통 스타일을 쉽게 적용 가능
동작/상태까지 함께 추상화한다면 → Component
- 타입으로 props와 상태를 안전하게 관리할 수 있음
- 클릭 핸들러, 로딩/disabled, 접근성 같은 복잡한 로직을 한 곳에 캡슐화 가능
- 여러 화면에서 동일한 UX 패턴을 재사용하기에 적합
결국 핵심은 “무엇을 추상화하고 있는가?” 입니다.
- 스타일만 공통화하고 싶다면: 유틸리티 클래스가 더 자연스럽고
- 동작과 로직까지 함께 다루고 싶다면: 컴포넌트로 추상화하는 편이 더 적절합니다.