이 글에서 정리하는 내용
React에서 Tailwind CSS를 사용할 때 className이 길어지는 문제를 JSX 가독성 관점에서 정리합니다. 긴 클래스 자체를 없애는 것보다, 반복되는 조합을 어디까지 상수로 빼고 언제 컴포넌트로 분리할지 판단하는 기준에 초점을 맞춥니다.
- Tailwind className이 길어지는 건 자연스러운 현상이다
- 진짜 문제는 길이가 아니라 수정 기준이 흐려지는 것이다
- 반복되는 조합은 상수로 먼저 분리한다
- UI 의미가 생기면 컴포넌트로 분리한다
- 조건부 클래스가 늘어난 뒤의 관리 기준
- 정리: 긴 className은 구조를 나눌 타이밍이다
Tailwind 이 길어지는 건 자연스러운 현상이다

React에서 Tailwind CSS를 쓰다 보면 JSX 안의 className이 생각보다 빨리 길어집니다. 카드 하나만 만들어도 mx-auto, flex, max-w-5xl, rounded-2xl, border, bg-white, p-6, shadow-sm, md:flex-row 같은 클래스가 한 줄에 붙습니다. 처음에는 조금 낯설지만, Tailwind가 작은 유틸리티 클래스를 조합해 마크업 안에서 스타일을 구성하는 방식이기 때문에 어느 정도는 자연스러운 결과입니다.
이 지점에서 바로 “Tailwind는 코드가 지저분해진다”로 결론을 내리면 문제를 잘못 보게 됩니다. 작은 화면을 빠르게 만들 때는 CSS 파일을 오가며 클래스 이름을 새로 짓는 것보다, 필요한 유틸리티를 JSX에서 바로 조합하는 방식이 더 빠릅니다. Tailwind의 기본 클래스가 기존 CSS 속성과 어떻게 연결되는지 아직 헷갈린다면 Tailwind CSS 기본 클래스: text font bg spacing 사용 기준을 먼저 정리해두면 이 글의 기준도 더 쉽게 이어집니다.
function PageHeader() { return ( <div className="mx-auto flex max-w-5xl flex-col gap-6 rounded-2xl border border-gray-200 bg-white p-6 shadow-sm md:flex-row md:items-center md:justify-between"> <div> <p className="text-sm font-medium text-blue-600">Dashboard</p> <h1 className="mt-2 text-2xl font-bold text-gray-950">주문 관리</h1> </div> <button className="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700"> 새 주문 등록 </button> </div> );
} 이 코드는 틀린 코드가 아닙니다. 페이지 상단 레이아웃을 한 번만 만들고 끝난다면 그대로 두어도 큰 문제가 생기지 않습니다. 다만 비슷한 카드, 헤더, 버튼이 다른 페이지에도 반복되기 시작하면 이야기가 달라집니다. JSX를 열었을 때 화면 구조보다 클래스 문자열이 먼저 눈에 들어오기 때문입니다.
진짜 문제는 길이가 아니라 수정 기준이 흐려지는 것이다
긴 className을 볼 때 바로 “짧게 줄여야겠다”라고 판단하면 기준이 흔들리기 쉽습니다. 실제로 불편한 부분은 길이가 아니라, 나중에 수정할 때 어느 클래스가 어떤 역할인지 한눈에 나뉘지 않는다는 점입니다. 예를 들어 위 코드에서 max-w-5xl, mx-auto는 바깥 레이아웃에 가깝고, rounded-2xl, border, shadow-sm은 카드의 시각 스타일에 가깝습니다. md:flex-row, md:items-center, md:justify-between은 반응형 배치와 연결됩니다.
이 역할이 한 줄에 섞이면 처음 작성한 사람은 기억할 수 있어도, 며칠 뒤 수정하거나 다른 사람이 이어받을 때는 다시 해석해야 합니다. 카드 UI에서 border, rounded, flex, items-center, justify-between 같은 레이아웃 기본 조합이 자주 헷갈린다면 Tailwind border flex 사용법: 레이아웃 기본 클래스 기준처럼 속성 단위로 끊어보는 방식이 잘 맞습니다.
그렇다고 한 번 쓰는 클래스까지 모두 변수로 빼면 오히려 읽을 위치가 늘어납니다. JSX에서는 구조를 보고 싶은데, 실제 스타일 문자열은 파일 위쪽이나 다른 파일에 흩어질 수 있습니다. 따라서 먼저 확인할 기준은 “이 클래스가 긴가?”가 아니라 “이 조합이 다시 등장하는가?”입니다.
| 상황 | 처리 기준 |
|---|---|
| 한 화면에서 한 번만 쓰는 레이아웃 | 그대로 둔다 |
| 같은 클래스 조합이 여러 번 반복됨 | 상수로 분리한다 |
| 카드, 버튼, 배지처럼 UI 의미가 있음 | 컴포넌트로 분리한다 |
| 상태와 variant가 계속 늘어남 | 조건부 클래스 조합 기준을 따로 둔다 |
반복되는 조합은 상수로 먼저 분리한다
가장 부담이 적은 방법은 반복되는 Tailwind 클래스 조합을 문자열 상수로 분리하는 것입니다. 컴포넌트 구조를 크게 바꾸지 않으면서 JSX 안에서 화면 구조를 더 잘 보이게 만들 수 있습니다. 특히 페이지 안에서만 쓰는 레이아웃 클래스라면 별도 컴포넌트로 빼기 전에 상수 분리만으로도 충분한 경우가 많습니다.
const pageHeaderClassName = "mx-auto flex max-w-5xl flex-col gap-6 rounded-2xl border border-gray-200 bg-white p-6 shadow-sm md:flex-row md:items-center md:justify-between"; function PageHeader() { return ( <div className={pageHeaderClassName}> <div> <p className="text-sm font-medium text-blue-600">Dashboard</p> <h1 className="mt-2 text-2xl font-bold text-gray-950">주문 관리</h1> </div> <button className="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700"> 새 주문 등록 </button> </div> );
} 상수로 분리하면 JSX에서는 className={pageHeaderClassName}만 보입니다. 덕분에 태그 안쪽의 콘텐츠 구조가 더 눈에 들어옵니다. 다만 이 방법은 어디까지나 “긴 문자열을 이름 붙여 옮긴 것”입니다. 같은 조합을 여러 곳에서 재사용할 때는 괜찮지만, 카드 내부 마크업까지 반복된다면 문자열 상수만으로는 부족합니다.
상수 이름은 너무 추상적으로 잡지 않는 편이 낫습니다. boxClassName, wrapperClassName처럼 범위가 넓은 이름은 나중에 수정할 때 다시 헷갈립니다. pageHeaderClassName, orderCardClassName, filterPanelClassName처럼 실제 화면 역할이 드러나야 수정할 위치를 더 빨리 찾을 수 있습니다.
UI 의미가 생기면 컴포넌트로 분리한다
반복되는 카드, 버튼, 배지, 섹션 헤더는 단순한 클래스 묶음이 아니라 UI 단위로 보는 편이 자연스럽습니다. 이때는 상수로 줄이는 것보다 컴포넌트로 분리하는 쪽이 더 오래 갑니다. React에서 컴포넌트는 마크업을 나누는 기술이라기보다, 화면에서 반복되는 의미를 이름 붙이는 단위에 가깝습니다. 이 기준은 React 컴포넌트 쉽게 이해하기: UI 재사용 구조 잡기와도 연결됩니다.
type CardProps = { children: React.ReactNode;
}; function Card({ children }: CardProps) { return ( <article className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm"> {children} </article> );
} function OrderSummaryCard() { return ( <Card> <p className="text-sm font-medium text-gray-500">이번 달 주문</p> <strong className="mt-2 block text-2xl font-bold text-gray-950">128건</strong> </Card> );
} 이렇게 나누면 Card는 카드의 공통 외형을 맡고, OrderSummaryCard는 카드 안에 들어갈 실제 정보를 맡습니다. JSX에서 읽어야 할 정보가 사라지는 것이 아니라 역할이 나뉩니다. 카드의 배경색이나 테두리, 그림자 값을 수정해야 할 때도 Card 컴포넌트를 먼저 확인하면 됩니다.
단, 컴포넌트를 너무 빨리 쪼개면 반대로 읽기 어려워질 수 있습니다. 한 번만 쓰는 박스를 Wrapper, Inner, Content, Body처럼 나누면 코드가 짧아진 것처럼 보여도 실제로는 이동해야 할 파일과 컴포넌트 이름이 늘어납니다. 분리 기준은 “짧아지는가”가 아니라 “반복되는 의미가 있는가”입니다.
JSX 가독성은 클래스 길이와만 연결되지 않는다
React JSX 안에서는 조건부 렌더링, 리스트 렌더링이벤트 핸들러, children 구조가 함께 섞입니다. 여기에 긴 Tailwind 클래스까지 들어오면 한 컴포넌트가 여러 책임을 동시에 갖는 것처럼 보입니다. 이럴 때는 클래스만 줄이기보다 컴포넌트 안에서 레이아웃, 데이터 표시, 상태 처리 중 무엇이 섞여 있는지 먼저 나눠보는 편이 좋습니다.
조건부 클래스가 늘어난 뒤의 관리 기준

Tailwind className이 가장 지저분해지는 순간은 상태가 들어올 때입니다. active, disabled, selected, error 같은 값에 따라 배경색, 테두리, 투명도, hover 스타일이 달라지면 템플릿 문자열 안에 삼항 연산자가 계속 늘어납니다. 이 상태가 한두 개일 때는 괜찮지만, variant까지 들어오면 JSX에서 실제 UI 구조를 읽기 어려워집니다.
type FilterButtonProps = { selected?: boolean; disabled?: boolean; children: React.ReactNode;
}; function FilterButton({ selected, disabled, children }: FilterButtonProps) { return ( <button disabled={disabled} className={`rounded-xl px-4 py-2 text-sm font-semibold ${ selected ? "border border-blue-600 bg-blue-50 text-blue-700" : "border border-gray-200 bg-white text-gray-700" } ${disabled ? "cursor-not-allowed opacity-50" : "hover:bg-gray-50"}`} > {children} </button> );
} 이 정도까지는 읽을 수 있습니다. 하지만 상태가 하나 더 늘어나거나 size, variant, fullWidth 같은 옵션이 추가되면 문자열 조합이 컴포넌트의 대부분을 차지합니다. 이때부터는 클래스 조합 방식을 따로 잡는 편이 낫습니다.
const filterButtonBaseClassName = "rounded-xl px-4 py-2 text-sm font-semibold transition"; const filterButtonStateClassName = { selected: "border border-blue-600 bg-blue-50 text-blue-700", default: "border border-gray-200 bg-white text-gray-700 hover:bg-gray-50", disabled: "cursor-not-allowed opacity-50"}; function getFilterButtonClassName({ selected, disabled}: { selected?: boolean; disabled?: boolean;
}) { const stateClassName = selected ? filterButtonStateClassName.selected : filterButtonStateClassName.default; return `${filterButtonBaseClassName} ${stateClassName} ${ disabled ? filterButtonStateClassName.disabled : "" }`;
} function FilterButton({ selected, disabled, children }: FilterButtonProps) { return ( <button disabled={disabled} className={getFilterButtonClassName({ selected, disabled })} > {children} </button> );
} 여기서 중요한 점은 객체나 함수로 분리했다고 무조건 좋은 코드가 되는 것은 아니라는 점입니다. 상태가 적고 한 번만 쓰는 버튼이라면 처음 코드가 더 읽기 쉬울 수 있습니다. 반대로 같은 버튼이 여러 필터 영역에서 반복되고, 선택 상태와 비활성 상태가 계속 바뀐다면 클래스 조합을 분리해두는 쪽이 나중에 수정하기 편합니다.
상태 스타일링이 부모, 형제, 자식 관계까지 얽히면 group, peer, has-* 같은 variant를 쓰게 됩니다. 이 경우에는 단순히 className 길이만 볼 것이 아니라 상태가 어디에서 발생하고 어느 요소가 바뀌는지를 먼저 나누어야 합니다. 이 부분은 Tailwind CSS group peer has 차이: 상태 기준으로 고르는 방법과 연결해서 보면 기준을 잡기 쉽습니다.
@apply보다 먼저 컴포넌트 구조를 확인한다
긴 Tailwind 클래스를 보면 CSS 파일로 옮기고 싶어집니다. @apply를 사용하면 여러 유틸리티 클래스를 하나의 CSS 클래스처럼 묶을 수 있기 때문에 짧아 보이는 효과는 분명 있습니다. 다만 React 프로젝트에서 UI가 반복되는 문제라면 먼저 컴포넌트 분리로 해결할 수 있는지 확인하는 편이 좋습니다.
예를 들어 카드 외형이 반복된다면 .card 클래스를 만드는 것보다 Card 컴포넌트를 만드는 쪽이 더 많은 정보를 담습니다. Card라는 이름 안에는 마크업 구조, 공통 스타일, children을 받는 방식까지 함께 들어가기 때문입니다. 반대로 서드파티 HTML에 Tailwind 스타일을 입히거나, CSS 레이어에서 재사용해야 하는 경우라면 Tailwind @apply 기준: @utility custom variant 차이처럼 별도 기준으로 판단하는 것이 맞습니다.
정리: 긴 은 구조를 나눌 타이밍이다
Tailwind 클래스가 길어졌다고 해서 바로 잘못된 코드는 아닙니다. Tailwind는 작은 유틸리티 클래스를 조합해서 화면을 만드는 도구이기 때문에, 어느 정도 긴 className은 자연스럽게 생깁니다. 대신 그 코드가 반복되거나, 상태가 늘어나거나, JSX에서 화면 구조보다 스타일 문자열이 먼저 보인다면 관리 기준을 다시 잡아야 합니다.
한 번만 쓰는 레이아웃은 그대로 둡니다. 같은 클래스 조합이 반복되면 상수로 분리합니다. 카드, 버튼, 배지처럼 UI 의미가 분명해지면 컴포넌트로 나눕니다. 상태와 variant가 늘어나면 조건부 클래스 조합을 별도로 관리합니다. 이 순서로 보면 className을 무조건 짧게 만들려고 하기보다, 나중에 수정할 사람이 어디를 보면 되는지 남길 수 있습니다.
참고 자료로는 Tailwind CSS의 utility-first styling, reusing styles 관련 공식 문서와 React의 JSX 및 className 관련 문서를 기준으로 개념을 확인했습니다. 본문 예시는 React 컴포넌트에서 카드 UI와 필터 버튼을 작성할 때 자주 마주치는 구조를 기준으로 재구성했습니다.