원문: https://cekrem.github.io/posts/tailwind-targeting-child-elements/
테일윈드(Tailwind)의 핵심은 유틸리티 클래스를 요소에 직접 적용하는 것입니다. p나 div 같은 일반 요소에 하위 선택자를 사용해 스타일을 적용하는 것은 본질에 반하는 방식입니다. 바로 그런 방식을 대체하기 위해 테일윈드가 설계되었기 때문입니다.
하지만 어쩔 수 없는 경우도 있습니다. CMS 콘텐츠, 타사 컴포넌트, 동적으로 생성된 HTML 같은 경우죠. 이렇듯 통제할 수 없는 요소에 스타일을 적용해야 할 때가 있습니다.
먼저 분명히 하자면, 이를 처리하기 위해 기본 CSS를 조금 추가하는 것이 때로는 가장 간단하고 합리적인 해결책입니다. CMS 콘텐츠 전용 스타일시트를 만드는 것도 완벽히 타당한 접근법입니다. 하지만 테일윈드의 유틸리티 클래스 패러다임 내에서 작업하기로 결심했거나, 단순히 가능한 방법을 궁금해한다면, 이 글에서 임의의 변형(arbitrary variant)을 사용해 자식 요소를 타겟팅하는 방법을 소개합니다.
제어할 수 없는 HTML이 포함된 컨테이너가 있다고 가정해 보겠습니다.
<div class="cms-content">
<p>Some text with a <a href="#">link</a> in it.</p>
<ul>
<li>List item one</li>
<li>List item two</li>
</ul>
</div>
모든 내부 링크에 특정 스타일을 적용하고 싶습니다. 마우스 오버 시 밑줄 표시, 상속된 텍스트 색상과 다른 독특한 색상, 아마도 글꼴 두께도요. 전통적인 접근법? 사용자 정의 CSS를 작성하세요.
.cms-content a {
font-weight: 600;
text-decoration: none;
}
.cms-content a:hover {
text-decoration: underline;
}
.cms-content li {
list-style-type: disc;
margin-left: 1.5rem;
}
이 방식은 아주 타당한 접근법입니다. 그리고 때론 올바른 선택이기도 하죠! CMS 콘텐츠용 소형 스타일시트는 단순하고 유지보수가 용이합니다. 하지만 모든 요소를 유틸리티 클래스로 관리하고 싶다면 테일윈드가 어떤 기능을 제공하는지 살펴보겠습니다.
테일윈드의 임의 변형을 사용하면 대괄호 표기법을 통해 클래스 이름에 직접 어떤 CSS 선택자든 작성할 수 있습니다.
<div
class="[&_a]:font-semibold [&_a]:no-underline [&_a:hover]:underline [&_li]:list-disc [&_li]:ml-6"
>
<p>Some text with a <a href="#">link</a> in it.</p>
<ul>
<li>List item one</li>
<li>List item two</li>
</ul>
</div>
처음에는 &_a 구문이 이상해 보일 수 있지만, 무슨 일이 일어나고 있는지 이해하면 간단합니다.
핵심은 &의 의미를 이해하는 데 있습니다. 테일윈드의 임의 변형에서 &는 현재 요소, 즉 클래스가 적용된 요소를 나타냅니다. 이는 Sass/SCSS나 CSS 중첩에서 &가 작동하는 방식과 정확히 동일합니다.
따라서 다음과 같이 작성해 봅시다.
<div class="[&_a]:font-semibold"></div>
테일윈드는 다음과 같은 CSS를 생성합니다.
.\[\&_a\]\:font-semibold a {
font-weight: 600;
}
클래스 이름은 이스케이프 처리됩니다(백슬래시들…), 하지만 중요한 부분은 a 후손 선택자입니다. &는 생성된 클래스 선택자로 대체된 후, 선택자(a)가 추가됩니다. 결과적으로 이 클래스를 가진 요소 내부의 어디에 있는 <a> 요소든 스타일이 적용됩니다.
다음은 유용한 자식 타겟팅 패턴입니다.
<!-- 모든 하위 자식 div에 테두리와 패딩 적용 -->
<div class="[&>div]:border [&>div]:p-4">...</div>
<!-- 첫 번째 하위 자식 요소는 상단 여백을 제거합니다 -->
<div class="[&>*:first-child]:mt-0">...</div>
<!-- 마지막 자식 요소는 하단 테두리를 제거합니다 -->
<div class="[&>*:last-child]:border-b-0">...</div>
<!-- 모든 링크에 마우스 오버 시 밑줄 적용 -->
<div class="[&_a]:no-underline [&_a:hover]:underline">...</div>
<!-- 모든 목록 항목에 원형 마커 적용 -->
<div class="[&_li]:list-disc [&_li]:ml-6">...</div>
<!-- 모든 이미지에 둥근 모서리 적용 -->
<div class="[&_img]:rounded-lg">...</div>
차이점 참고: >는 직접 자식 요소만 대상으로 하는 반면, 공백(테일윈드에서는 _로 표시)은 모든 후손 요소를 대상으로 합니다.
<!-- 자식 요소에 대한 호버 상태 -->
<div class="[&>button:hover]:bg-blue-600">...</div>
<!-- 비활성화된 입력란은 배경색이 어둡게 처리됨 -->
<form
class="[&_input:disabled]:bg-gray-100 [&_input:disabled]:cursor-not-allowed"
>
...
</form>
<!-- 중첩된 입력 필드의 포커스 스타일 -->
<div class="[&_input:focus]:ring-2 [&_input:focus]:ring-blue-500">...</div>
솔직히 말해, 내장된 콘텐츠의 스타일을 지정할 때는 기본 CSS 스타일시트가 더 나은 선택인 경우가 많습니다. 더 간단하고 가독성이 높으며 유지보수가 쉽기 때문입니다. 하지만 다음과 같은 경우에는 이 임의의 변형 접근법이 의미 있을 수 있습니다:
당신이 제어할 수 있는 콘텐츠의 경우, 요소에 직접 클래스를 적용하세요. 그것이 여전히 테일윈드의 방식이며, 솔직히 말해서 이 모든 것보다 더 간단합니다.
이 글을 쓰게 된 배경은 다음과 같습니다. 저희는 고객사에서 헤드리스 CMS의 글을 표시합니다. 콘텐츠는 미리 렌더링 된 HTML 형태로 도착하며, 저희는 이를 자체 컨테이너로 감쌉니다. 내부 마크업은 제어할 수 없습니다. CMS가 생성하는 단락, 링크, 목록, 이미지 등 무엇이든 포함될 수 있습니다. 구조와 사용된 요소(그리고 원하는 곳에 클래스를 추가할 수 없는 점)는 흥미로운 제약 조건을 추가합니다.
(참고: 내장된 콘텐츠는 항상 새니타이징하세요! 하지만 이 글의 범위를 벗어납니다.)
몇 가지 간단한 규칙을 넘어서는 경우, 전용 스타일시트를 사용하는 것이 일반적으로 더 깔끔합니다.
.cms-content a {
font-weight: 600;
}
.cms-content a:hover {
text-decoration: underline;
}
.cms-content img {
border-radius: 0.5rem;
max-width: 100%;
}
.cms-content li {
list-style-type: disc;
margin-left: 1.5rem;
}
이는 가독성이 높고 유지보수가 용이하며 특별한 구문을 배울 필요가 없습니다. 많은 프로젝트에서 이것이 올바른 답입니다.
하지만 컴포넌트에 스타일을 유지하기로 결정했다면, 다음과 같이 구현할 수 있습니다(여느 때처럼 Elm으로 작성).
viewArticleContent : List (Html msg) -> Html msg
viewArticleContent content =
Html.article
[ Attr.class "p-4"
, Attr.class "[&_a]:font-semibold [&_a:hover]:underline"
, Attr.class "[&_img]:rounded-lg [&_img]:max-w-full"
, Attr.class "[&_li]:list-disc [&_li]:ml-6"
]
content
또는 리액트로 구현할 수도 있습니다.
const ArticleContent = ({ children }) => (
<article
className="
p-4
[&_a]:font-semibold [&_a:hover]:underline
[&_img]:rounded-lg [&_img]:max-w-full
[&_li]:list-disc [&_li]:ml-6
"
>
{children}
</article>
);
모든 스타일링은 래퍼 컴포넌트에 포함됩니다. 디자인이 변경되면 한 곳에서 클래스를 업데이트하면 됩니다.
테일윈드에는 @tailwindcss/typography 플러그인도 있는데, prose 클래스가 리치 텍스트 스타일을 처리합니다. 운이 좋다면 이것만으로도 충분할 수 있지만, 때로는 더 세밀한 제어가 필요하거나 기존 디자인 시스템을 맞춰야 할 때도 있습니다.
[&…] 구문을 사용한 임의 변형은 테일윈드의 유틸리티 클래스 패러다임 내에서 사실상 모든 CSS 선택자를 작성할 수 있게 합니다. &는 해당 클래스가 적용된 요소를 나타내며, 그 뒤의 모든 것은 표준 CSS 선택자 구문(공백은 _로 대체)입니다.
이것이 최선의 방법일까요? 아마도 아닙니다! 내장 콘텐츠용 소규모 순수 CSS 스타일시트가 종종 더 간단하고 가독성이 높으며 팀이 유지 관리하기 쉽습니다. 테일윈드와 전통적인 CSS는 문제없이 공존할 수 있습니다.
하지만 테일윈드의 유틸리티 클래스 모델 내에서 작업해야 하거나(또는 해야만 한다면), 혹은 가능한 한계를 궁금해한다면, 이제 방법을 알게 되었습니다.