WEGO ) Tailwind CSS로 확장성과 재사용성을 고려한 공통 컴포넌트 설계하기
프로젝트를 진행하면서 버튼 컴포넌트와 같은 공통 컴포넌트를 설계할 때, 확장성과 재사용성을 최우선으로 고려했습니다. 이를 위해 cva, clsx, tailwind-merge라는 세 가지 라이브러리를 조합하여 사용했는데요, 이번 글에서는 각 라이브러리의 특징과 이들을 조합했을 때의 시너지에 대해 공유하고자 합니다.
▪︎ 각 라이브러리의 특징과 역할
▫︎ CVA (Class Variance Authority)
Class Variance Authority(CVA)는 컴포넌트의 다양한 스타일 변형(variants)을 타입 안전하게 관리할 수 있게 해주는 라이브러리입니다. 특히 Tailwind CSS와 함께 사용할 때 그 진가를 발휘하는데, 이는 미리 정의된 스타일 조합을 타입 시스템의 보호 아래 안전하게 사용할 수 있게 해줍니다.
우리 프로젝트의 버튼 컴포넌트에는 다음과 같이 variants를 정의해 사용하였습니다.
1 | const ButtonVariants = cva( |
이러한 CVA의 사용은 다음과 같은 구체적인 이점을 제공합니다.
- 타입 안전성: TypeScript와 완벽하게 통합되어, 정의되지 않은 variant 조합을 사용하려 할 때 컴파일 단계에서 오류를 발견할 수 있습니다. 예를 들어, fill=”invalid”와 같은 잘못된 값을 사용하려 하면 즉시 TypeScript 에러가 발생합니다.
- 중앙 집중화된 스타일 관리: 모든 스타일 variant를 한 파일에서 관리함으로써, 디자인 시스템의 일관성을 유지하기가 훨씬 쉬워집니다. 스타일 수정이 필요할 때도 한 곳만 수정하면 되므로 유지보수가 용이합니다.
- 자동 완성 지원: IDE에서 사용 가능한 variant 옵션들을 자동으로 제안받을 수 있어, 개발 생산성이 크게 향상됩니다.
▫︎ CLSX
CLSX는 조건부 클래스명을 처리하기 위한 유틸리티 라이브러리입니다. 특히 React 컴포넌트에서 동적인 클래스명을 다룰 때 매우 유용합니다. 이 라이브러리는 다양한 형태의 입력을 처리할 수 있으며, 최종적으로 유효한 클래스명 문자열을 생성해줍니다.
1 | import { clsx, ClassValue } from "clsx"; |
CLSX는 다음과 같은 강력한 기능들을 제공합니다.
- 다양한 입력 형식 지원: 문자열, 객체, 배열 등 어떤 형태로든 클래스명을 전달할 수 있습니다. 이는 다양한 상황에서 유연하게 클래스를 조합할 수 있게 해줍니다.
- 자동 정리: falsy 값(undefined, null, false 등)을 자동으로 필터링하여 깨끗한 클래스명 문자열을 생성합니다.
- 직관적인 문법: 복잡한 조건부 클래스 로직도 읽기 쉽고 이해하기 쉬운 형태로 작성할 수 있습니다.
▫︎ Tailwind Merge
Tailwind Merge는 Tailwind CSS를 사용할 때 발생할 수 있는 클래스 충돌 문제를 우아하게 해결해주는 라이브러리입니다. 특히 재사용 가능한 컴포넌트를 만들 때 매우 유용한데, 이는 기본 스타일과 커스텀 스타일이 충돌할 때 가장 마지막에 선언된 스타일을 우선적으로 적용하는 방식으로 동작합니다.
Tailwind Merge를 통해 해결할 수 있는 실제 문제 상황을 예시로 살펴보겠습니다.
1 | // Tailwind Merge 없이 사용할 경우 |
Tailwind Merge는 다음과 같은 핵심적인 문제들을 해결합니다.
- 충돌 해결: 같은 속성을 가진 클래스들이 충돌할 때, 가장 마지막에 선언된 클래스를 우선적으로 적용합니다. 이는 CSS의 일반적인 캐스케이딩 규칙과도 일치하는 직관적인 동작입니다.
- 최적화: 중복되거나 불필요한 클래스들을 자동으로 제거하여 최종적으로 깔끔한 클래스 문자열을 생성합니다.
- 모든 Tailwind 규칙 지원: Tailwind CSS의 모든 유틸리티 클래스와 변형자(modifiers)를 완벽하게 이해하고 처리할 수 있습니다.
▪︎ 세 라이브러리를 조합한 유틸함수 활용
이 세 라이브러리를 조합하여 사용할 때 발생하는 시너지는 매우 강력합니다. 각각의 라이브러리가 가진 장점들이 서로를 보완하면서, 더욱 강력하고 유지보수하기 쉬운 컴포넌트 시스템을 구축할 수 있게 됩니다.
우리 프로젝트에 적용하기 위해 세 라이브러리를 조합한 유틸함수인 cn을 작성하였습니다.
1 | // cn.tsx |
1 | / Button.tsx (유틸 함수 활용한 공통 컴포넌트) |
1. 개발 경험 향상
- 타입 안전성: CVA를 통해 정의된 variants는 TypeScript의 타입 검사를 받게 되어, 잘못된 prop 사용을 방지할 수 있습니다.
- 자동 완성: IDE에서 사용 가능한 모든 variant 옵션들을 자동으로 제안받을 수 있어 개발 속도가 향상됩니다.
- 디버깅 용이성: 스타일 관련 문제가 발생했을 때, 문제의 원인을 쉽게 추적할 수 있습니다.
2. 유지보수성 개선
- 중앙 집중화된 스타일 관리: 모든 기본 스타일 변형이 한 곳에서 관리되어 일관성 유지가 쉽습니다.
- 변경 용이성: 디자인 시스템의 변경이 필요할 때, variants 정의만 수정하면 되므로 변경 사항을 쉽게 적용할 수 있습니다.
- 코드 재사용: 잘 정의된 variant 시스템으로 인해 동일한 스타일을 여러 곳에서 일관되게 사용할 수 있습니다.
3. 확장성 확보
- 커스텀 스타일링: className prop을 통해 기본 스타일을 확장하거나 재정의할 수 있습니다.
- 조건부 스타일링: classNameCondition을 통해 특정 조건에 따른 스타일 적용이 가능합니다.
- 새로운 변형 추가: 새로운 디자인 요구사항이 생겼을 때 variants에 쉽게 추가할 수 있습니다.
▪︎ 컴포넌트 사용 예시
▫︎ 기본적인 사용 패턴
1 | // 기본 버튼 |
이러한 기본 사용에서는 미리 정의된 variants를 통해 일관된 디자인을 유지하면서도, 각 상황에 맞는 적절한 스타일을 적용할 수 있습니다.
▫︎ 활용 패턴
1 | // 동적 스타일링이 필요한 경우 |
활용 패턴에서는 다음과 같은 이점을 얻을 수 있습니다.
- className을 통해 상황에 따른 추가적인 스타일을 적용할 수 있습니다.
- classNameCondition을 사용하여 여러 조건에 따른 스타일 변화를 한 번에 관리할 수 있습니다.
- 기본 variants와 커스텀 스타일을 자연스럽게 조합할 수 있습니다.
▪︎ 유지보수와 확장
이러한 설계는 장기적인 관점에서 큰 이점을 제공합니다.
1. 새로운 디자인 요구사항 대응
- 새로운 variant를 추가하기 쉽습니다.
- 기존 스타일을 수정하더라도 타입 시스템이 변경이 필요한 곳을 알려줍니다.
2. 일관성 유지
- 모든 버튼이 동일한 스타일 시스템을 따르게 됩니다.
- 디자인 토큰의 변경이 용이합니다.
3. 팀 협업
- 명확한 사용 방법과 타입 지원으로 다른 개발자들도 쉽게 사용할 수 있습니다.
- 문서화가 용이합니다.
▪︎ 마치며
이번 글에서는 CVA, CLSX, Tailwind Merge를 조합하여 어떻게 확장 가능하고 유지보수하기 좋은 버튼 컴포넌트를 구현했는지 살펴보았습니다. 이러한 설계 방식을 통해 얻은 주요 이점들을 정리해보면 다음과 같습니다:
▫︎ 실제 프로젝트에서의 효과
- 개발 생산성 향상
- 타입 시스템의 지원으로 실수를 사전에 방지할 수 있었습니다.
- 자동 완성 기능으로 개발 속도가 크게 향상되었습니다.
- 반복적인 스타일링 코드 작성이 줄어들었습니다.
- 디자인 시스템 일관성 확보
- 중앙에서 관리되는 스타일 variants로 인해 일관된 UI를 유지할 수 있었습니다.
- 디자인 변경 사항을 쉽게 적용할 수 있었습니다.
- 새로운 디자인 요구사항에도 유연하게 대응할 수 있었습니다.
- 코드 품질 향상
- 타입 안전성으로 인해 런타임 에러가 감소했습니다.
- 스타일 관련 버그를 쉽게 추적하고 해결할 수 있었습니다.
- 코드베이스가 커져도 유지보수가 어렵지 않았습니다.
이러한 접근 방식은 단순히 스타일링 문제를 해결하는 것을 넘어서, 확장 가능하고 유지보수하기 좋은 컴포넌트 시스템을 구축하는 데 큰 도움이 되었습니다. 특히 팀 단위의 개발에서 일관성을 유지하면서도 유연한 확장이 가능한 구조를 만들 수 있었다는 점에서 큰 의미가 있었습니다.
앞으로도 이러한 경험을 바탕으로, 더 나은 컴포넌트 시스템을 구축해 나가도록 하겠습니다.