React

React Tailwind 조건부 클래스 정리: className이 길어질 때 나누는 기준

2025.12.04·수정 2026.05.11·약 15분

이 글에서 정리하는 내용

React에서 Tailwind CSS를 사용할 때 조건부 이 길어지는 이유와 정리 기준을 다룹니다. 짧은 조건은 JSX 안에서 처리하고, 반복되는 조합은 객체로 분리하며, 버튼 컴포넌트가 커졌을 때는 props 구조, 유틸 함수, variant 관리 방식까지 어떤 순서로 나눌지 정리합니다.

React에서 Tailwind 이 길어지는 순간

React에서 Tailwind className이 길어지는 구조와 조건부 클래스 문제를 설명하는 인포그래픽

React에서 Tailwind CSS를 쓰다 보면 이 길어지는 순간을 금방 만납니다. 버튼 하나를 만들 때는 정도만 붙이면 됩니다. 그런데 버튼이 활성 상태인지, 비활성 상태인지, primary 버튼인지, danger 버튼인지까지 들어오면 안에 스타일과 조건 판단이 같이 섞입니다.

처음에는 이 방식이 크게 불편하지 않습니다. React에서는 JSX 안에서 JavaScript 조건식을 사용할 수 있으므로, 삼항 연산자나 조건으로 UI를 바꾸는 흐름 자체는 자연스럽습니다. JSX 조건 처리 흐름이 먼저 헷갈린다면 React JSX 문법 사용법: 조건부 렌더링과 리스트 처리를 먼저 확인하면 지금 다루는 문제도 더 쉽게 이어집니다.

문제는 조건이 늘어날 때입니다. 하나만 있을 때는 삼항 연산자로 충분하지만, 여기에가 붙기 시작하면 버튼 JSX가 UI 구조라기보다 스타일 조건표처럼 보입니다. 나중에 색상만 바꾸려 해도 어떤 조건이 실제 배경색을 결정하는지 다시 읽어야 합니다.

처음에는 이런 코드가 자연스럽다

function Button({ active, disabled }) { return ( <button disabled={disabled} className={`px-4 py-2 rounded text-sm font-medium ${ active ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700" } ${ disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700" }`} > 버튼 </button> );
}

이 코드는 틀린 코드가 아닙니다. 버튼이 한두 개이고 조건이 단순하면, 상태와 스타일을 한 자리에서 바로 볼 수 있어 수정이 빠릅니다. 다만 이 구조를 그대로 확장하면 문자열이 컴포넌트의 중심이 됩니다. 버튼이 어떤 역할을 하는지보다, 어떤 Tailwind 클래스가 어떤 조건에서 붙는지만 먼저 보이게 됩니다.

짧은 조건은 JSX 안에 둬도 된다

조건부 클래스가 보인다고 해서 바로 분리할 필요는 없습니다. 상태가 하나뿐이고같은 조합이 다른 곳에서 반복되지 않는다면 JSX 안에서 처리하는 쪽이 더 읽기 쉽습니다. 너무 이른 분리는 파일을 옮겨 다니며 읽게 만들고, 단순한 버튼 하나를 이해하는 데 오히려 시간이 더 걸립니다.

기준은 코드 길이 자체보다 “수정할 때 한 번에 판단되는가”입니다. 아래처럼 여부에 따라 배경색과 텍스트 색상만 바뀌는 정도라면 삼항 연산자로 충분합니다. 조건이 짧고, 바뀌는 클래스의 목적도 분명합니다.

function FilterButton({ active, children }) { return ( <button className={`rounded-full px-4 py-2 text-sm ${ active ? "bg-slate-900 text-white" : "bg-slate-100 text-slate-600" }`} > {children} </button> );
}

필터 탭처럼 “선택됨 / 선택 안 됨”만 구분하는 UI라면 이 정도가 더 낫습니다. 별도 객체를 만들면 오히려를 찾아가야 합니다. Tailwind를 쓴다고 해서 모든 클래스를 밖으로 빼야 하는 것은 아닙니다. 짧은 조건은 JSX 안에 두고, 반복되거나 조합이 늘어나는 순간에 분리하는 흐름이 실제 작업에서 덜 흔들립니다.

반복되기 시작하면 객체 매핑으로 분리한다

버튼이처럼 목적별로 나뉘면 이야기가 달라집니다. 이때부터는 삼항 연산자를 계속 늘리는 방식보다 공통 클래스와 variant 클래스를 분리하는 편이 낫습니다. 공통 모양은 목적별 색상은 에 두면 수정 위치가 분명해집니다.

이 방식은 별도 라이브러리 없이 적용할 수 있고, React 초급 단계에서도 이해하기 쉽습니다. 버튼 컴포넌트 자체의 재사용 기준이 함께 헷갈린다면 React 컴포넌트 쉽게 이해하기: UI 재사용 구조 잡기와 연결해서 보는 것이 좋습니다. 정리는 결국 재사용 컴포넌트의 props 설계와 같이 움직입니다.

const buttonBase = "rounded px-4 py-2 text-sm font-medium transition-colors"; const buttonVariants = { primary: "bg-blue-600 text-white hover:bg-blue-700", secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200", danger: "bg-red-600 text-white hover:bg-red-700"}; function Button({ variant = "primary", disabled, children }) { return ( <button disabled={disabled} className={[ buttonBase, buttonVariants[variant], disabled && "cursor-not-allowed opacity-50"] .filter(Boolean) .join(" ")} > {children} </button> );
}

여기서 핵심은 클래스 문자열을 억지로 짧게 만드는 것이 아닙니다. 공통으로 유지되는 부분과 조건에 따라 바뀌는 부분을 나누는 것입니다.은 버튼의 기본 모양입니다. 반면은 버튼의 목적에 따라 달라집니다. 이 둘이 한 문자열 안에 섞여 있으면 나중에 버튼 높이를 바꾸는 일과 danger 색상을 바꾸는 일이 같은 위치에서 뒤섞입니다.

배열 조합에 을 쓰면 조건이 false인 값은 빠지고, 남은 문자열만 합쳐집니다. 이 패턴은 간단한 컴포넌트에서는 충분합니다. 다만 조건이 계속 늘어나면 배열 안이 다시 길어집니다. 그때는 문자열 조합 방식이 아니라 컴포넌트 구조를 먼저 확인해야 합니다.

variant가 많아지면 컴포넌트 구조를 다시 본다

버튼이 커지는 흐름은 대체로 비슷합니다. 처음에는 색상만 나누다가, 어느 순간 크기와 너비로딩 상태가 추가됩니다. 관리자 화면의 “저장”, “삭제”, “취소”, “미리보기” 버튼을 같은 컴포넌트로 묶으려 하면 만으로는 부족해집니다.

이때는 Tailwind 클래스를 더 줄이기 전에 Button 컴포넌트가 받을 props를 먼저 정리해야 합니다. 예를 들어이 각각 어떤 역할을 하는지 나눠두면 조합도 자연스럽게 분리됩니다.

const buttonBase = "inline-flex items-center justify-center rounded font-medium transition-colors"; const buttonVariants = { primary: "bg-blue-600 text-white hover:bg-blue-700", secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200", danger: "bg-red-600 text-white hover:bg-red-700"}; const buttonSizes = { sm: "h-8 px-3 text-xs", md: "h-10 px-4 text-sm", lg: "h-12 px-5 text-base"}; function Button({ variant = "primary", size = "md", fullWidth = false, disabled = false, loading = false, children}) { const isDisabled = disabled || loading; return ( <button disabled={isDisabled} className={[ buttonBase, buttonVariants[variant], buttonSizes[size], fullWidth && "w-full", isDisabled && "cursor-not-allowed opacity-50"] .filter(Boolean) .join(" ")} > {loading ? "처리 중" : children} </button> );
}

이 정도가 되면 정리는 단순 문자열 문제가 아니라 컴포넌트 API 문제에 가깝습니다. 는 색상과 목적을 담당하고는 높이와 글자 크기를 담당하며는 레이아웃 참여 방식을 담당합니다. 이렇게 역할을 나누면 클래스를 바꿀 때 어느 객체를 수정해야 하는지 바로 보입니다.

상태 스타일링이 버튼 내부에서 끝나지 않고 부모, 형제, 자식 관계까지 번지면 기준이 더 복잡해집니다. 예를 들어 카드 안의 radio가 선택되었을 때 카드 전체를 강조하거나, input의 상태에 따라 옆 텍스트 색을 바꾸는 상황입니다. 이런 흐름은 단순한 조합보다 Tailwind CSS group peer has 차이: 상태 기준으로 고르는 방법에서 다루는 상태 기준과 같이 봐야 합니다.

disabled와 hover가 섞일 때 확인할 부분

조건부 클래스에서 자주 놓치는 부분은 와 의 관계입니다. 버튼에 속성을 넣으면 클릭이나 폼 제출 동작은 막을 수 있습니다. 하지만 같은 스타일 규칙이 코드에서 자동으로 사라지는 것은 아닙니다. 스타일 문자열만 보면 활성 상태와 비활성 상태의 기준이 한 덩어리에 남아 있습니다.

Tailwind에는 같은 상태 variant도 있기 때문에 단순한 경우에는 처럼 처리할 수 있습니다. 다만 React에서 값을 props로 관리하고 있고, hover 효과를 아예 붙이지 않으려는 구조라면 조건을 분리하는 편이 더 명확합니다., 기준을 함께 정리하려면 Tailwind CSS hover, focus-visible, disabled, aria, data variant 정리와 연결해서 보면 됩니다.

const buttonVariants = { primary: "bg-blue-600 text-white", secondary: "bg-gray-100 text-gray-700", danger: "bg-red-600 text-white"}; const buttonHoverVariants = { primary: "hover:bg-blue-700", secondary: "hover:bg-gray-200", danger: "hover:bg-red-700"}; function Button({ variant = "primary", disabled, children }) { return ( <button disabled={disabled} className={[ "rounded px-4 py-2 text-sm font-medium transition-colors", buttonVariants[variant], !disabled && buttonHoverVariants[variant], disabled && "cursor-not-allowed opacity-50"] .filter(Boolean) .join(" ")} > {children} </button> );
}

이렇게 하면 disabled일 때 hover 클래스 자체를 붙이지 않습니다. 코드가 조금 늘어나지만, 상태 기준은 더 분명해집니다. 버튼 색상과 hover 색상을 꼭 분리해야 한다는 뜻은 아닙니다. 비활성 상태에서 상호작용 스타일이 같이 남아 헷갈린다면, hover를 별도 매핑으로 빼는 방식이 읽기 편합니다.

또 하나는 arbitrary variant가 섞이는 경우입니다. 예를 들어같은 선택자 기반 클래스까지 버튼 안에 들어오면 은 더 빨리 길어집니다. 대괄호 문법까지 함께 쓰는 상황이라면 Tailwind Arbitrary Value Variant 차이: 대괄호 문법 사용 기준처럼 선택자 조건을 따로 정리해두는 편이 낫습니다.

유틸 함수와 cva는 언제 검토할까

React Tailwind 조건부 클래스를 객체 매핑, cn 유틸, cva 구조로 나누는 기준을 정리한 이미지

배열 조합과 객체 매핑만으로도 많은 경우는 정리됩니다. 하지만 여러 컴포넌트에서 같은 패턴이 반복되면 같은 유틸 함수를 두는 편이 낫습니다. 이 버튼, 카드, 배지, 탭 컴포넌트마다 반복된다면 그 자체가 중복입니다.

export function cn(...classes) { return classes.filter(Boolean).join(" ");
}
function Button({ variant = "primary", disabled, children }) { return ( <button disabled={disabled} className={cn( buttonBase, buttonVariants[variant], disabled && "cursor-not-allowed opacity-50" )} > {children} </button> );
}

이 단계까지는 별도 라이브러리가 없어도 됩니다. 다만 객체, 배열, 조건부 문자열이 계속 늘어나고 충돌까지 자주 생긴다면 같은 유틸리티를 검토할 수 있습니다. 는 조건에 따라 문자열을 조합하는 용도로 많이 쓰입니다. 여기에 Tailwind 클래스 충돌까지 정리해야 한다면 프로젝트에 따라 를 함께 쓰는 방식도 선택할 수 있습니다.

버튼, 배지, 알림, 카드처럼 variant 체계가 디자인 시스템 수준으로 커졌다면 도 후보가 됩니다. 는 base, variants, compoundVariants, defaultVariants 같은 구조로 클래스 조합을 관리할 수 있습니다. 처음부터 도입할 필요는 없지만같은 variant 규칙을 여러 컴포넌트에서 반복해서 만들고 있다면 검토할 만합니다.

CSS 파일로 빼는 방식도 선택지입니다. 다만 Tailwind를 쓰는 상황에서 단순히 이 길다는 이유만으로 를 남발하면, 다시 CSS 파일 안에 Tailwind 조합이 숨을 수 있습니다. 이 기준은 Tailwind @apply 기준: @utility custom variant 차이와 같이 보면 판단하기 쉽습니다.

정리

React에서 Tailwind 조건부 클래스가 길어지는 문제는 “어떻게 하면 한 줄로 줄일까”보다 “어떤 기준으로 나눌까”가 먼저입니다. 조건이 하나뿐이면 JSX 안의 삼항 연산자가 충분합니다. 같은 조합이 반복되면로 분리하고, 컴포넌트가 커지면처럼 props 역할을 먼저 나눠야 합니다.

처음부터 도구를 많이 넣을 필요는 없습니다. 버튼 하나를 정리하는 단계에서는 객체 매핑과 배열 조합만으로도 충분합니다. 여러 컴포넌트에서 같은 조합 코드가 반복될 때 이나 를 검토하고, 디자인 시스템처럼 variant 규칙이 커질 때 를 생각하면 됩니다.

다음에 버튼이나 카드 컴포넌트를 수정할 때는 클래스 길이만 보지 말고, 공통 모양과 상태 변화가 같은 위치에 섞여 있는지 먼저 확인하면 됩니다. Tailwind 클래스가 긴 것은 흔한 일입니다. 다만 그 긴 문자열 안에서 무엇이 기본값이고, 무엇이 상태이며, 무엇이 컴포넌트의 variant인지 구분되지 않는다면 그때가 분리할 시점입니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

“React Tailwind 조건부 클래스 정리: className이 길어질 때 나누는 기준”에 대한 1개의 생각

댓글 남기기