원인부터 말하면 Cascading 때문입니다.
Cascading 알고리즘에는 다양한 순서가 적용되는데 그 순서로 인한 문제였습니다.
tailwindcss 를 사용하면서 아래와 같이 조건에 따라 hidden 을 넣어 주려고 했습니다.
export default function App() {
const [isVisible, setIsVisible] = useState(false);
return (
<>
<button onClick={() => setIsVisible(prev => !prev)}>Toggle</button>
<div className={`flex-with-center w-20 h-10 bg-red-200 ${isVisible ? 'hidden' : ''}`}></div>
</>
);
}
그런데 개발자도구의 요소탭을 보니 제가 정의한 커스텀 속성인 flex-with-center 속성이 tailwindcss에 정의되어 있는 hidden 보다 높은 우선순위로 적용이 됐습니다.

그런데 flex-with-center 를 제거하고 tailwindcss에 정의되어 있는 속성을 사용하니 hidden이 조건에 따라 변경 됐습니다.
export default function App() {
const [isVisible, setIsVisible] = useState(false);
return (
<>
<button onClick={() => setIsVisible(prev => !prev)}>Toggle</button>
<div className={`flex justify-center items-center w-20 h-10 bg-red-200 ${isVisible ? 'hidden' : ''}`}></div>
</>
);
}
아마 Cascading에 대해 몰랐으면 이 때 뭐가 문제인지 몰랐을겁니다.
요소탭을 봐도 잘 적용된걸 볼 수 있습니다.

CSS는 Cascading Style Sheets의 약자인거 알고 계셨나요?
저는 사실 모르고 있었습니다...
Cascading은 아래와 같은 뜻을 가지고 있스빈다.

즉, CSS는 계단식 스타일 시트 혹은 위에서 아래로 떨어지는 스타일 시트라고 할 수 있습니다.
그리고 MDN을 보면 Cascading은 CSS에서 핵심 개념이며, 다양한 소스에서 발생하는 속성 값을 결합하는 방법을 정의하는 알고리즘 이라고 정의하고 있습니다.
조금 말을 쉽게 바꿔보면 출처, @layer, @scope 에서의 우선순위를 설정하는 알고리즘 입니다.
파일의 출처는 세가지가 있습니다.
첫째, 브라우저가 기본으로 제공하는 User-agent stylesheets
둘째, 개발자가 정의하는 Author stylesheets
셋째, 유저가 정의할 수 있는 User stylesheets
세가지의 우선순위는 User-agent stylesheets가 제일 낮고 User stylesheets, Author stylesheets 순으로 높아집니다.
그리고 세가지 출처 안에서는 @layer의 선언 순위를 기반으로 우선순위를 정합니다.
예를들어 아래와 같이 정의되어 있다면 base, components, utilities 순서대로 스타일이 적용되게 됩니다. 익숙한가요? 맞습니다! Tailwindcss가 이렇게 되어있죠!
@utilities가 먼저 선언됐지만 정의는 components가 먼저 됐기에 item의 color는 rebeccapurple 색이 됩니다!
무슨색이죠?
// global.css
@layer base;
@layer components;
@layer utilities;
@layer utilities {
.item {
color: rebeccapurple;
}
}
@layer components {
.item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
}
}
다음으로 명시도 라는 개념이 있습니다. 이는 브라우저가 어느 요소와 가장 연관된 속성을 찾는 수단으로, 이렇게 찾은 속성이 해당 요소에 적용됩니다. 명시도는 여러 종류의 CSS selector로 구성된 일치 규칙에 기반합니다.
계산하는 방법도 존재하는데요. 명시도는 CSS 선언에 적용되는 가중치 입니다. 그래서 일치하는 선택자수에 따라 가중치를 두고, 높은 가중치를 가진 선언이 선택되는 방식입니다. 한번 봐볼까요?
아래의 선택자들은 유형별로 명시도를 증가시킵니다.
이를 계산할 때는 (3)-(2)-(1) 순으로 개수를 셉니다.
그래서 앞의 가중치가 높은 선언이 선택되게 됩니다.
한번 예시를 보겠습니다.
div#test span {
color: green;
}
div span {
color: blue;
}
span {
color: red;
}
위의 선언들의 명시도를 계산하면 아래와 같습니다.
1. div#test span: 아이디 선택자 1, 클래스 선택자 0, 유형 선택자 2 → 1-0-2
2. div span: 아이디 선택자 0, 클래스 선택자 0, 유형 선택자 2 → 명시도: 0-0-2
3. span: 아이디 선택자 0, 클래스 선택자 0, 유형 선택자 1 → 명시도: 0-0-1
위 명시도에 따라 아래의 span은 초록색이 될겁니다.
<div id="test"> <span> hi </span> </div>
그런데 이렇게 말로 하는것보다 아래 짤이 설명이 너무 잘 되어있어서 이걸 보는게 이해하기 더 쉬울지도 모릅니다.

참고로 명시도가 높아도 인라인 스타일을 적용한다면 인라인 스타일이 적용되고, 인라인 스타일을 적용해도 !important를 적어두면 !important를 명시해둔 스타일이 적용됩니다. 그런데 !importatn는 정말 꼭 필요한 순간에만 써야합니다.
이제 기본적인 개념을 한번 쭉 봤으니 전체적인 순서를 봐보겠습니다.
| 우선순위(낮음에서 높음) | 출처 | !important 여부 |
|---|---|---|
| 1-1 | user-agent - 첫 번째로 선언된 @layer | |
| 1-2 | user-agent - 마지막으로 선언된 @layer | |
| 1-3 | user-agent - layer 밖의 스타일 | |
| 2-1 | user - 처음으로 선언된 @layer | |
| 2-2 | user - 마지막으로 선언된 @layer | |
| 2-3 | user - layer 밖의 스타일 | |
| 3-1 | author - 첫 번째로 선언된 @layer | |
| 3-2 | author - 마지막으로 선언된 @layer | |
| 3-3 | author - layer 밖의 스타일 | |
| 3-4 | inline style | |
| 4 | @keyframes | |
| 5-1 | author - layer 밖의 스타일 | !important |
| 5-2 | author - 마지막으로 선언된 @layer | !important |
| 5-3 | author - 첫 번째로 선언된 @layer | !important |
| 5-4 | inline style | !important |
| 6-1 | user - layer 밖의 스타일 | !important |
| 6-2 | user - 마지막으로 선언된 @layer | !important |
| 6-3 | user - 처음으로 선언된 @layer | !important |
| 7-1 | user-agent - layer 밖의 스타일 | !important |
| 7-2 | user-agent - 마지막으로 선언된 @layer | !important |
| 7-3 | user-agent - 첫 번째로 선언된 @layer | !important |
| 8 | trasition |
위에 inline style, @keyframres, transition에 !important가 없는걸 눈치 채셨나요? 맞습니다. 해당 방법들은 !important가 적용되지 않기 때문입니다.
tailwindcss를 설정할 때 아래처럼 css파일을 만든 기억이 있으신가요?
@layer가 어떤 순위로 적용될지 선언해주기 위해 이렇게 적는다는걸 이제 알 수 있습니다.
@tailwind base;
@tailwind components;
@tailwind utilities;
기존에 flex-with-center 클래스는 @utilities 안에 존재했습니다.
그런데 .hidden 도 같은 @utilities 안에 존재합니다.
그래서 제가 추가적으로 정의한 flex-with-center는 Tailwindcss의 클래스들보다 더 아래에 정의되고, 더 높은 우선순위를 갖습니다.
컴파일된 후의 css는 아래와 같은 모습을 하고있을 겁니다.
...
@layer utilities {
...
/* tailwindcss가 정의한 클래스 */
.hidden { }
/* 내가 정의한 클래스 */
.flex-with-center { }
}
원인을 알았으니 문제 해결은 간단합니다.
css 파일 맨 아래에 hidden 클래스를 추가하거나 flex-with-center와 hidden을 동시에 적용하지 않으면 됩니다 혹은 @layer components에 특정 클래스를 추가해 hidden보다 위에 위치 시켜주면 됩니다.
참고로 tailwindcss의 preflight - 공식문서가 한번 적용되고 tailwindcss에서 정의한 클래스들이 선언됩니다.
어떻게 선언됐는지 궁금하면 tailwindcss github(tailwindcss/src/corePlugins.js)를 참고해주세요!