본문 바로가기

FrontEnd

[번역] 왜 Tailwind CSS를 사용하나요?

반응형

Tailwind CSS가 CSS를 작성하는 최고의 방법인지에 대해선 논란이 있으나,

꽤 다수가 동의하는건 CSS에 충분히 혼쭐나기 전까지는 얼마나 효과적인지 모른다는 것입니다.

Tailwind CSS 창시자가 Tailwind CSS를 만들게 된 배경을 살펴봅니다.

아래 원문를 의역한 글입니다.

 

https://adamwathan.me/css-utility-classes-and-separation-of-concerns/

 

CSS Utility Classes and "Separation of Concerns"

Software developer, author, and host of Full Stack Radio. August 7, 2017

adamwathan.me

Tailwind CSS logo

TL;DR

답은 유틸리티 퍼스트 클래스를 이용한 관심사의 분리입니다.

  • 클래스 명이라는 안정적인 인터페이스 기반으로 디자인 토큰과 html 문서 구조 간에 느슨한 결합을 만들 수 있습니다.
    • 마크업에 하드코딩된 클래스 명은 강결합 처럼 보이지만 실제적으로는 느슨하게 결합된 효과를 가져옵니다.
    • CSS를 최소한으로 변경하며, 필요한 경우 마크업을 변경합니다.
    • CSS 명은 구조와 의미가 아니라 스타일과 레이아웃에 기반합니다.
  • 스타일이 필요할 경우 html에서 유틸리티 클래스를 추가하여 사용하고, 구조 변경이 필요할 경우 html 구조만 수정합니다.
    • 디자인 토큰이 수정되어도 html을 변경할 필요가 없습니다.
    • 컴포넌트와 디자인 규칙은 독립적으로 진화할 수 있습니다.

 

 

 


저는 지난 몇 년 동안 의미론적 CSS 작성에서 기능 중심 CSS 작성으로 작업 방식을 전환해 왔습니다.
이런 식으로 CSS를 작성하는 것은 많은 개발자들로부터 꽤 본능적인 반발(a pretty visceral reaction)을 일으킬 수 있습니다.

저는 이 게시물에서 제가 작업 방식을 변경한 이유와 그 과정에서 얻은 교훈과 통찰력을 공유하고자 합니다.


1. 의미론적 CSS

CSS를 잘하는 방법을 배우려고 할 때 듣게 될 모범 사례 중 하나는 "관심사의 분리"입니다.
HTML에 콘텐츠에 대한 정보만 포함되어야 하고(구조와 의미)
CSS에서 모든 스타일 결정이 이루어져야 한다는 것입니다.(스타일과 레이아웃)

 
이 HTML을 보세요
<p class="text-center">
    Hello there!
</p>

text-center 클래스가 보이시나요?
텍스트를 중앙에 배치하는 것은 디자인 적 결정 사항입니다.
이 코드는 스타일 정보가 HTML로 흘러 들어가도록 했기 때문에 "관심사의 분리"를 위반합니다.

 

권장되는 접근 방식은 콘텐츠 기반 클래스 명을 지정하고
해당 클래스를 CSS의 훅으로 사용하여 마크업 스타일을 지정하는 것입니다.

<style>
.greeting {
    text-align: center;
}
</style>

<p class="greeting">
    Hello there!
</p>

CSS Zen Garden는 이 방식의 장점을 잘 보여줍니다.
"관심사를 분리"하면 스타일시트를 교체하는 것만으로 사이트를 완전히 재설계할 수 있음을 보여주기 위해 설계된 사이트입니다.

제 워크플로는 다음과 같았습니다.

1. 새로운 UI(이 경우 저자 약력 카드)에 필요한 마크업을 작성합니다.

<div>
  <img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
  <div>
    <h2>Adam Wathan</h2>
    <p>
      Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
    </p>
  </div>
</div>

2. 컨텐츠에 따라 설명 클래스를 한두 개 추가합니다.

<div class="author-bio">
    <img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
    <div>
      <h2>Adam Wathan</h2>
      <p>
        Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
      </p>
    </div>
  </div>

3. 내 CSS/Less/Sass에서 이러한 클래스를 "훅"으로 사용하여 내 새 마크업의 스타일을 지정합니다.

.author-bio {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
  > img {
    display: block;
    width: 100%;
    height: auto;
  }
  > div {
    padding: 1rem;
    > h2 {
      font-size: 1.25rem;
      color: rgba(0,0,0,0.8);
    }
    > p {
      font-size: 1rem;
      color: rgba(0,0,0,0.75);
      line-height: 1.5;
    }
  }
}
다음은 최종 결과의 데모입니다.
 

See the Pen Author Bio, nested selectors by Adam Wathan (@adamwathan) on CodePen.

 
이 접근 방식은 직관적이었으며, 저는 한동안 HTML과 CSS를 이렇게 작성했습니다.

하지만 뭔가 이상하다는 생각이 들었습니다.

 

나는 "내 관심사를 분리"했지만 여전히 내 CSS와 HTML 사이에 매우 분명한 결합이 있었습니다.
즉, 내 CSS는 내 마크업의 거울과 같았습니다.

중첩된 CSS 셀렉터는 내 HTML 구조를 완벽하게 반영합니다.


내 마크업은 스타일 결정사항에 관심이 없었지만,

CSS는 내 마크업 구조에 매우 관심이 있었습니다.
관심사의 분리가 충분하지 않은 것 같습니다.


2. 마크업 구조에서 스타일 분리하기

 
이 문제를 해결하기 위해 마크업에 더 많은 클래스를 추가하기 시작했습니다.
셀렉터의 특이성을 낮게 유지하고 CSS를 특정 DOM 구조에 덜 의존하게 만듭니다.
 

이 아이디어를 옹호하는 가장 잘 알려진 방법론은 Block Element Modifer 또는 BEM입니다.
BEM과 유사한 접근 방식을 취하면 저자 약력(author-bio) 컴포넌트 마크업은 다음과 같이 보일 수 있습니다.

<div class="author-bio">
  <img class="author-bio__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
  <div class="author-bio__content">
    <h2 class="author-bio__name">Adam Wathan</h2>
    <p class="author-bio__body">
      Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
    </p>
  </div>
</div>
CSS는 다음과 같을 것입니다:
.author-bio {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.author-bio__image {
  display: block;
  width: 100%;
  height: auto;
}
.author-bio__content {
  padding: 1rem;
}
.author-bio__name {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.author-bio__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}

저는 이를 엄청난 발전이라 느꼈습니다.
내 마크업은 여전히 ​​"의미론적"이지만, 스타일 결정사항을 포함하지 않았으며
이제 내 CSS는 불필요한 셀렉터 특정성을 피하는 추가 보너스와 함께 내 마크업 구조에서 분리된 느낌을 받았습니다.

 

하지만 곧 딜레마에 빠졌습니다.


3. 유사한 컴포넌트 다루기

사이트에 새로운 기능을 추가해야 한다고 가정해 보겠습니다.
카드 레이아웃으로 기사 미리보기를 표시하는 것입니다.

언뜻 보기에 저자 약력 컴포넌트와 거의 유사한 것 같습니다.

우리의 관심사를 분리하면서 이 문제를 처리하는 가장 좋은 방법은 무엇일까요?
.author-bio 클래스를 .article-preview 컴포넌트에 적용하는 것은 의미론적인 솔루션이 아닙니다.
따라서 .article-preview를 자체 컴포넌트로 만들어야 합니다.

마크업은 다음과 같습니다.
<div class="article-preview">
  <img class="article-preview__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
  <div class="article-preview__content">
    <h2 class="article-preview__title">Stubbing Eloquent Relations for Faster Tests</h2>
    <p class="article-preview__body">
      In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
    </p>
  </div>
</div>
이제, CSS는 어떻게 할까요?

옵션 1 : 복붙하기

한 가지 접근 방식은 .author-bio 스타일을 그대로 복사하고 클래스 이름을 바꾸는 것입니다.
.article-preview {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.article-preview__image {
  display: block;
  width: 100%;
  height: auto;
}
.article-preview__content {
  padding: 1rem;
}
.article-preview__title {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.article-preview__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}

이 솔루션은 매우 DRY하지 않습니다.

또한, 저자 약력 컴포넌트와 유사하게 수정해야 할 경우,

일관되지 않은 디자인으로 이어질 수 있습니다.

옵션 2 : @extend 저자 약력 컴포넌트

또 다른 접근 방식은 선택한 전처리기의 @extend 기능을 사용하는 것입니다.
.author-bio 컴포넌트에 이미 정의된 스타일을 다시 사용할 수 있습니다.
.article-preview {
  @extend .author-bio;
}
.article-preview__image {
  @extend .author-bio__image;
}
.article-preview__content {
  @extend .author-bio__content;
}
.article-preview__title {
  @extend .author-bio__name;
}
.article-preview__body {
  @extend .author-bio__body;
}

@extend를 사용하는 것은 일반적으로 권장되지 않지만(generally not recommended),
CSS에서 중복을 제거했으며 마크업에는 스타일 관심사가 없습니다.
하지만 한 가지 옵션을 더 살펴보겠습니다.

옵션 3 : 컨텐츠에 구애받지 않는 컴포넌트 만들기

.author-bio 및 .article-preview 컴포넌트는 "의미론적" 관점에서 공통점이 없습니다.
하나는 저자의 약력이고 다른 하나는 기사의 미리보기입니다.

하지만, 그들은 디자인 관점에서 많은 공통점을 가지고 있습니다.
즉, 공통된 이름을 따서 명명된 컴포넌트를 만들고 두 타입의 콘텐츠에 해당 컴포넌트를 재사용할 수 있습니다.

 

컴포넌트 명은 media-card라고 합시다.

.media-card {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.media-card__image {
  display: block;
  width: 100%;
  height: auto;
}
.media-card__content {
  padding: 1rem;
}
.media-card__title {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.media-card__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}
저자 약력 컴포넌트의 마크업은 다음과 같습니다.
<div class="media-card">
  <img class="media-card__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
  <div class="media-card__content">
    <h2 class="media-card__title">Adam Wathan</h2>
    <p class="media-card__body">
      Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
    </p>
  </div>
</div>
기사 미리보기 컴포넌트 마크업은 다음과 같습니다.
<div class="media-card">
  <img class="media-card__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
  <div class="media-card__content">
    <h2 class="media-card__title">Stubbing Eloquent Relations for Faster Tests</h2>
    <p class="media-card__body">
      In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
    </p>
  </div>
</div>
이 접근 방식은 CSS에서 중복을 제거하지만 관심사가 섞인게 아니냐는 우려를 일으킬 수 있습니다.

마크업은 이 두 콘텐츠가 모두 media-card로 스타일이 지정되기를 원한다는 것을 알게 되었습니다.
하지만 이제, 기사 미리보기의 모양을 변경하지 않고 저자 약력의 모양을 변경하려면 어떤 작업을 해야 하죠?

이전에는 스타일시트를 열고 두 컴포넌트 중 하나에 대한 스타일만 변경하면 되었습니다.
이제 HTML을 수정해야 합니다! 신성 모독입니다!

 

그러나 잠시 이면에 대해 생각해 봅시다.
동일한 스타일이 필요한 새로운 타입의 콘텐츠를 추가해야 하는 경우 어떻게 해야 하죠?

  • "의미론적" 접근 방식을 위해
    • HTML을 작성하고,
    • 클래스를 CSS의 훅으로 사용하기 위해 HTML에 일부 콘텐츠별 클래스를 추가하고
    • 스타일시트를 열고, 새 콘텐츠 타입에 대한 새 CSS 컴포넌트를 만들고, 공유 스타일을 적용합니다.
    • 유사한 CSS를 복붙하거나, @extend 또는 @mixin을 사용합니다.

콘텐츠에 구애받지 않는 .media-card 클래스를 사용하는 경우 새 HTML만 작성하면 됩니다.
스타일시트를 전혀 열 필요가 없습니다.
정말 관심사가 섞인 것이라면, 우리는 더 많은 곳을 변경해야 하지 않았을까요?


관심사의 분리는 허수아비 입니다

"관심사의 분리"라는 관점에서 HTML과 CSS는 흑과 백입니다.
당신은 관심사의 분리가 있거나(좋습니다!) 없습니다(나쁨!).
이것은 HTML과 CSS에 대해 생각하는 올바른 방법이 아닙니다.
대신 의존성의 방향에 대해 생각하십시오.
 
HTML과 CSS를 작성하는 방법에는 두 가지가 있습니다.

1. 관심사의 분리(Separation of Concerns)

HTML에 의존하는 CSS (콘텐츠 기반 CSS 클래스명, CSS -> HTML)

콘텐츠(예: .author-bio)를 기반으로 클래스 이름을 지정하면 HTML을 CSS의 의존성으로 취급합니다.

HTML은 독립적입니다. 스타일과 무관하게

HTML이 제어하는 ​​.author-bio와 같은 후크를 노출합니다.

 

반면에 CSS는 독립적이지 않습니다.

HTML이 노출하기로 결정한 클래스를 알아야 하고

HTML 스타일을 지정하기 위해 해당 클래스를 대상으로 지정해야 합니다.

이 모델에서 HTML은 스타일을 변경할 수 있지만 CSS는 재사용할 수 없습니다.

2. 관심사 섞기(Mixing Concerns)

CSS에 의존하는 HTML (HTML은 스타일을 나타내는 클래스명에 의해 CSS에 의존함. HTML -> CSS)

 
UI에서 반복되는 패턴(예: .media-card)을 따라 콘텐츠에 구애받지 않는 방식으로 클래스 이름을 지정하면
CSS가 HTML의 종속성으로 취급됩니다.
CSS는 독립적입니다.
어떤 콘텐츠에 적용되는지는 상관하지 않고 마크업에 적용할 수 있는 컴포넌트 집합만 노출합니다.
 
HTML은 독립적이지 않습니다.
CSS에서 제공한 클래스를 사용하고 있으며
원하는 디자인을 달성하는 데 필요한 클래스를 결합할 수 있도록 어떤 클래스가 존재하는지 알아야 합니다.
이 모델에서 CSS는 재사용할 수 있지만 HTML은 스타일을 변경할 수 없습니다.

CSS Zen Garden은 첫 번째 접근 방식을 취하고
Bootstrap이나 Bulma와 같은 UI 프레임워크는 두 번째 접근 방식을 취합니다. (tailwind CSS도 이 방식!)

 

둘 다 "틀린" 것이 아닙니다. 특정 상황에서 더 중요한 것이 무엇인지에 따라 내린 결정일 뿐입니다.
스타일을 변경할 수 있는 HTML과 재사용 가능한 CSS 중 어느 것이 더 가치가 있나요?

3. 재사용성을 선택하기

Nicolas Gallagher의 About HTML semantics and front-end architecture는 제 전환점이 되었습니다.
재사용 가능한 CSS를 위한 최적화가 프로젝트에 올바른 선택이 될 것이라고 확신했습니다.


4. 유사한 컴포넌트 다루기

내 목표는 콘텐츠를 기반으로 하는 클래스를 만드는 것을 피하는 것이 아닙니다.
가능한 한 재사용 가능한 방식으로 모든 클래스명 지정하는 것입니다.

그 결과 다음과 같은 클래스 이름이 생성되었습니다.
  • .card
  • .btn, .btn--primary, .btn--secondary
  • .badge
  • .card-list, .card-list-item
  • .img--round
  • .modal-form, .modal-form-section
  • ...등등

재사용 가능한 클래스를 만드는 데 집중하기 시작했을 때 새로운 것을 알게 되었습니다.
컴포넌트가 많을수록, 또는 컴포넌트가 더 구체적일수록 재사용하기가 더 어렵습니다.

다음은 직관적인 예입니다.
몇 개의 양식 섹션이 있고 하단에 제출 버튼이 있는 양식을 작성 중이라고 가정해 보겠습니다.
모든 양식 콘텐츠를 .stacked-form 컴포넌트의 일부로 생각했다면
제출 버튼에 .stacked-form__button과 같은 클래스를 제공할 수 있습니다.

<form class="stacked-form" action="#">
  <div class="stacked-form__section">
    <!-- ... -->
  </div>
  <div class="stacked-form__section">
    <!-- ... -->
  </div>
  <div class="stacked-form__section">
    <button class="stacked-form__button">Submit</button>
  </div>
</form>

하지만 같은 스타일을 사용하는 버튼이 해당 양식 밖에 있을 수도 있습니다.

이 버튼에 .stacked-form__button 클래스를 사용하는 것은 의미가 없습니다.

이 두 버튼은 모두 해당 페이지의 primary action이므로
컴포넌트의 공통점을 기반으로 버튼 이름을 지정하여 btn--primary라고 부르고
.stacked-form__ 접두사를 완전히 제거하면 어떻게 될까요?
  <form class="stacked-form" action="#">
    <!-- ... -->
    <div class="stacked-form__section"> 
      <button class="btn btn--primary">Submit</button>
    </div>
  </form>

이제 이 stacked-form이 플로팅된 카드에 있는 것처럼 보이길 원한다고 가정해 보겠습니다.
한 가지 접근 방식은 수정자(modifier)를 만들어 다음 폼에 적용하는 것입니다.
하지만 이미 .card 클래스가 있는 경우 기존 카드와 stacked form을 사용하여 이 새 UI를 구성하면 어떨까요?

<div class="card">
    <form class="stacked-form" action="#">
      <!-- ... -->
    </form>
</div>

이 접근 방식을 사용하면 모든 콘텐츠의 컨테이너가 될 수 있는 .card와
또한, 모든 컨테이너 내부에서 사용할 수 있는 .stacked-form이 있습니다.

우리는 컴포넌트에서 더 많은 재사용성을 얻고 있으며 새로운 CSS를 작성할 필요가 없었습니다.


5. 서브컴포넌트를 이용한 합성

stacked-form의 맨 아래에 다른 버튼을 추가해야 하고
기존 버튼에서 약간 공간적으로 떨어져 있기를 원한다고 가정해 보겠습니다.

<form class="stacked-form" action="#">
  <!-- ... -->
  <div class="stacked-form__section">
    <button class="btn btn--secondary">Cancel</button>
    <!-- Need some space in here -->
    <button class="btn btn--primary">Submit</button>
  </div>
</form>

한 가지 접근 방식은 .stacked-form__footer와 같은 새 서브 컴포넌트를 만들고
.stacked-form__footer-item과 같은 추가 클래스를 각 버튼에 추가하고
자손 선택자를 사용하여 여백을 추가하는 것입니다.

  <form class="stacked-form" action="#">
    <!-- ... -->

   <div class="stacked-form__section stacked-form__footer">
     <button class="stacked-form__footer-item btn btn--secondary">Cancel</button>
     <button class="stacked-form__footer-item btn btn--primary">Submit</button>
    </div>
  </form>
다음은 CSS의 모습입니다.
.stacked-form__footer {
  text-align: right;
}
.stacked-form__footer-item {
  margin-right: 1rem;
  &:last-child {
    margin-right: 0;
  }
}

그러나 어딘가의 subnav 또는 header에서 이와 동일한 문제가 발생한다면 어떻게 될까요?

 

.stacked-form 외부에서 .stacked-form__footer를 재사용할 수 없으므로
헤더 내부에 새 하위 컴포넌트를 만들 수 있습니다.

  <header class="header-bar">
    <h2 class="header-bar__title">New Product</h2> 
    <div class="header-bar__actions">
      <button class="header-bar__action btn btn--secondary">Cancel</button>
      <button class="header-bar__action btn btn--primary">Save</button>
    </div>
  </header>

하지만 우리는 새로운 .header-bar__actions 컴포넌트에
.stacked-form__footer를 빌드하는 데 들인 노력을 똑같이 들여야 합니다.

 

콘텐츠 기반 클래스 이름을 사용할 때 처음에 겪었던 문제와 많이 비슷하지 않나요?
이 문제를 해결하는 한 가지 방법은 재사용하기 쉽고 합성 가능한 완전히 새로운 컴포넌트를 찾는 것입니다.
아마도 우리는 .actions-list와 같은 것을 만들 것입니다.

.actions-list {
  text-align: right;
}
.actions-list__item {
  margin-right: 1rem;
  &:last-child {
    margin-right: 0;
  }
}

이제 .stacked-form__footer 및 .header-bar__actions 컴포넌트를 완전히 제거하고
대신 두 상황 모두에서 .actions-list를 사용할 수 있습니다.

<!-- Stacked form -->
<form class="stacked-form" action="#">
  <!-- ... -->
  <div class="stacked-form__section">
    <div class="actions-list">
      <button class="actions-list__item btn btn--secondary">Cancel</button>
      <button class="actions-list__item btn btn--primary">Submit</button>
    </div>
  </div>
</form>

<!-- Header bar -->
<header class="header-bar">
  <h2 class="header-bar__title">New Product</h2>
  <div class="actions-list">
    <button class="actions-list__item btn btn--secondary">Cancel</button>
    <button class="actions-list__item btn btn--primary">Save</button>
  </div>
</header>

그러나 이러한 작업 목록 중 하나를 왼쪽 정렬하고 다른 하나는 오른쪽 정렬해야 한다면 어떻게 될까요?
.actions-list--left 및 .actions-list--right 수정자를 만들면 될까요?


6. 콘텐츠에 구애받지 않는 컴포넌트 + 유틸리티 클래스

항상 이러한 컴포넌트 이름을 생각해 내기 위해 노력하는 것은 지겹습니다.

.actions-list--left와 같은 모디파이어를 만드는 것은,

하나의 CSS 속성을 할당하기 위해 완전히 새로운 컴포넌트 모디파이어를 만드는 일입니다.

그것은 이미 이름에 남아 있으므로 어떤 식으로든 "의미론적"으로 사람을 속일 수 없습니다.

 

왼쪽 정렬 및 오른쪽 정렬 모디파이어가 필요한 다른 컴포넌트가 있는 경우
새 컴포넌트를 위해서 또 다시 모디파이어도 만들어야 하나요?

 

이것은 우리가 .stacked-form__footer 및 .header-bar__actions를 없애고
단일 .actions-list로 교체하기로 결정했을 때 직면했던 동일한 문제로 돌아갑니다.

반복 대신 합성을 선호합니다.

두 개의 action 리스트가 있는 경우
하나는 왼쪽으로 정렬해야 하고 다른 하나는 오른쪽으로 정렬해야 하는 경우
합성으로 이 문제를 어떻게 해결할 수 있을까요?

정렬 유틸리티 클래스

합성으로 이 문제를 해결하려면
원하는 효과를 제공하는 재사용 가능한 새 클래스를 컴포넌트에 추가할 수 있어야 합니다.

 

우리는 이미 모디파이어를 .actions-list--left 및 .actions-list--right라고 부르려고 했으므로
이러한 새 클래스를 .align-left 및 .align-right와 같은 이름으로 부르지 않을 이유가 없습니다.

.align-left {
  text-align: left;
}
.align-right {
  text-align: right;
}
이제 컴포지션을 사용하여 스택 폼 버튼을 왼쪽 정렬할 수 있습니다.
<form class="stacked-form" action="#">
  <!-- ... -->
  <div class="stacked-form__section">
    <div class="actions-list align-left">
      <button class="actions-list__item btn btn--secondary">Cancel</button>
      <button class="actions-list__item btn btn--primary">Submit</button>
    </div>
  </div>
</form>
... 그리고 헤더 버튼을 오른쪽 정렬할 수 있습니다.
<header class="header-bar">
  <h2 class="header-bar__title">New Product</h2>
  <div class="actions-list align-right">
    <button class="actions-list__item btn btn--secondary">Cancel</button>
    <button class="actions-list__item btn btn--primary">Save</button>
  </div>
</header>

두려워하지 마세요

HTML에서 "left" 및 "right"라는 단어를 보는 것이 불편하다면
UI에서 시각적 패턴의 이름을 따서 명명된 컴포넌트를 오랫동안 사용해 왔다는 것을 기억하세요.

.stacked-form은 더 이상 .align-right보다 "의미론적"인 척 하지 않습니다.
사실 둘 다 마크업의 표현에 영향을 미치는 방식에 따라 이름이 정해져 있습니다.
특정 표현 결과를 달성하기 위해 마크업에서 해당 클래스를 사용하고 있습니다.

우리는 CSS 의존적인 HTML을 작성 중입니다. (HTML -> CSS)

최대한 CSS를 수정하지 않으며
양식을 .stacked-form에서 .horizontal-form으로 변경하려면 CSS가 아닌 마크업을 수정합니다.

불필요한 추상화를 제거하세요

이 솔루션의 흥미로운 점은 .actions-list 컴포넌트가 이제 쓸모가 없다는 것입니다.
이전에 해당 컴포넌트의 역할은 단지 왼쪽 정렬하는 것 뿐이었습니다.

.actions-list__item만 남기고 삭제합시다

  .actions-list__item {
    margin-right: 1rem;
    &:last-child {
      margin-right: 0;
    }
  }

.actions-list 없이 .actions-list__item이 존재하는 것이 조금 이상합니다.
.actions-list__item 컴포넌트를 생성하지 않고 원래 문제를 해결할 수 있는 다른 방법이 있을까요?

우리가 이 컴포넌트를 만든 이유는 두 버튼 사이에 약간의 여백을 추가하기 위해서였습니다.
.actions-list는 일반적이고 상당히 재사용이 가능하기 때문에 버튼 목록에 대한 꽤 괜찮은 은유였습니다.
하지만 "action-list"안에 있는 게 아닌 아이템 사이에도 동일한 양의 간격이 필요한 상황이 있을 수 있습니다. 맞습니까?

재사용 가능한 이름은 .spaced-horizontal-list일까요?
스타일링이 필요한 것은 자식 요소 뿐이기 때문에 실제 .actions-list 컴포넌트는 이미 삭제했습니다.

Spacer 유틸리티

자식들만 스타일링이 필요한 경우 pseudo selector를 사용하여 그룹으로 스타일을 지정하는 대신
독립적으로 자식들의 스타일을 지정하는 것이 더 간단할까요?

아이템 옆에 공백을 추가하는 가장 재사용 가능한 방법은 "이 요소는 옆에 공백이 있어야 합니다"라고 말하는 클래스입니다.

우리는 이미 .align-left 및 .align-right와 같은 유틸리티를 추가했습니다.
오른쪽 여백을 추가하기 위해 새 유틸리티를 만든다면 어떨까요?

요소의 오른쪽에 약간의 여백을 추가하기 위해 .mar-r-sm과 같은 새 유틸리티 클래스를 만들어 보겠습니다.
.mar-r-sm {
  margin-right: 1rem;
 }
이제 양식과 헤더가 다음과 같이 보일 것입니다.
<!-- Stacked form -->
<form class="stacked-form" action="#">
  <!-- ... -->
  <div class="stacked-form__section align-left">
    <button class="btn btn--secondary mar-r-sm">Cancel</button>
    <button class="btn btn--primary">Submit</button>
  </div>
</form>

<!-- Header bar -->
<header class="header-bar">
  <h2 class="header-bar__title">New Product</h2>
  <div class="align-right">
    <button class="btn btn--secondary mar-r-sm">Cancel</button>
    <button class="btn btn--primary">Save</button>
  </div>
</header>

.actions-list(마크업)의 개념은 어디에도 없으며
CSS는 더 작으며

클래스는 더 많이 재사용할 수 있습니다.


7. 유틸리티 퍼스트 CSS

재사용과 합성 가능성을 우선한 CSS 클래스
일반적인 시각적 조정을 위해 아래와 같은 유틸리티 클래스들을 만들 수 있습니다.
  • Text sizes, colors, and weights
  • Border colors, widths, and positions
  • Background colors
  • Flexbox utilities
  • Padding and margin helpers

놀라운 점은 새로운 CSS를 작성하지 않고도 완전히 새로운 UI 컴포넌트를 만들 수 있다는 것입니다.
아래와 같은 컴포넌트를 새로 만들었다고 가정해봅시다.

제 마크업은 다음과 같습니다.
<div class="card rounded shadow">
    <a href="..." class="block">
        <img class="block fit" src="...">
    </a>
    <div class="py-3 px-4 border-b border-dark-soft flex-spaced flex-y-center">
        <div class="text-ellipsis mr-4">
            <a href="..." class="text-lg text-medium">
                Test-Driven Laravel
            </a>
        </div>
        <a href="..." class="link-softer">
            @icon('link')
        </a>
    </div>
    <div class="flex text-lg text-dark">
        <div class="py-2 px-4 border-r border-dark-soft">
            @icon('currency-dollar', 'icon-sm text-dark-softest mr-4')
            <span>$3,475</span>
        </div>
        <div class="py-2 px-4">
            @icon('user', 'icon-sm text-dark-softest mr-4')
            <span>25</span>
        </div>
    </div>
</div>
사용된 클래스의 수로 인해 처음에는 당황할 수 있습니다.
원한다면 유틸리티 클래스들을 나열하는 대신 CSS 클래스 명으로 합칠 수 있습니다.
물론, 콘텐츠와 관련된 이름을 사용하고 싶지 않습니다.
이렇게 하면 컴포넌트를 하나의 컨텍스트에서만 사용할 수 있기 때문입니다.
아마도 이런 것이겠죠?
.image-card-with-a-full-width-section-and-a-split-section { ... }

대신 우리는 이전에 이야기한 것처럼 작은 컴포넌트를 이용해 합성하고 싶습니다.

 

합성할 수 있는 작은 컴포넌트는 무엇일까요?
위의 카드 컴포넌트에서 해답을 얻을 수 있습니다.

 

모든 카드에 그림자가 있는 것은 아니므로 해당 카드에서 .card--shadowed 수정자를 사용하거나,
모든 컴포넌트에서 .shadow를 사용하는 것입니다.
후자가 더 재사용 가능해 보이니 후자로 합시다.

 

우리 사이트의 일부 카드는 모서리가 둥글지 않지만, 이 카드는 둥급니다.
우리는 그것을 .card-rounded로 만들 수 있지만, 때때로 카드가 아닌 모서리 둥근 요소가 있습니다.
.rounded가 더 재사용 가능해 보입니다.
 
맨 위에 있는 이미지는 어떨까요?
.img--fitted와 같은 것을 사용하면 될까요?
사이트에는 부모 너비에 맞춰야 하는 몇 가지 다른 컴포넌트이 있으며 이 컴포넌트들이 항상 이미지는 아닙니다.
아마도 .fit 헬퍼가가 더 나을 것입니다.

이제 당신은 제 의도를 알 수 있습니다. 재사용성에 중점을 두고 제가 한 작업을 이해하면,
재사용 가능한 유틸리티를 사용해 컴포넌트를 구축하는 것이 자연스러운 목적지입니다.


8.유틸리티 퍼스트 클래스의 장점

강제된 일관성

작고 합성 가능한 유틸리티를 사용할 때의 가장 큰 이점 중 하나는

팀의 모든 개발자가 항상 고정된 옵션 집합에서 값을 선택한다는 것입니다.

 

HTML의 스타일을 지정하고 "이 텍스트는 좀 더 어두워야 합니다"라고 생각한 다음
기본 $text-color를 조정하기 위해 darken() 함수를 사용한 적이 몇 번이나 있나요?

이 글꼴은 좀 더 작아야 하기에 작업 중인 컴포넌트에 font-size: .85em을 추가한 적이 있나요?

 

임의의 값이 아니라 상대적인 색상이나 상대적인 글꼴 크기를 사용하고 있기 때문에 "올바른" 일을 하고 있는 것처럼 느껴집니다.

그러나 컴포넌트에 대해 텍스트를 10% 어둡게 하기로 결정하였으나.
다른 사람이 해당 컴포넌트를 대해 12% 어둡게 하면 어떻게 될까요?

스타일시트에 402개의 고유한 텍스트 색상이 나타납니다.(402 unique text colors in your stylesheet)

이 현상은 새로운 CSS를 작성해 스타일을 지정하는 모든 코드베이스에서 발생합니다.
  • GitLab: 402 text colors, 239 background colors, 59 font sizes
  • Buffer: 124 text colors, 86 background colors, 54 font sizes
  • HelpScout: 198 text colors, 133 background colors, 67 font sizes
  • Gumroad: 91 text colors, 28 background colors, 48 font sizes
  • Stripe: 189 text colors, 90 background colors, 35 font sizes
  • GitHub: 163 text colors, 147 background colors, 56 font sizes
  • ConvertKit: 128 text colors, 124 background colors, 70 font sizes

css 변수나 믹스인을 통해 일관성을 시도하고 적용할 수 있지만
새로운 CSS의 모든 라인은 여전히 ​​새로운 복잡성의 기회입니다.
더 많은 CSS를 추가한다고 해서 CSS가 더 단순해지지는 않습니다.

 

대신 스타일을 지정하는 솔루션이 기존 클래스를 적용하는 것이라면 이 문제가 해결됩니다.

텍스트를 약간 어둡게? .text-dark-soft 클래스를 추가합니다.
폰트를 약간 작게? .text-sm 클래스를 추가합니다.

 

프로젝트의 모든 사람이 선별된 제한된 옵션 집합에서 스타일을 선택하면
CSS가 프로젝트 크기에 따라 선형적으로 커지는 것에서 벗어나 일관성을 얻을 수 있습니다.

탬플릿(prop)보다 유틸리티 클래스(클래스명을 직접 사용하기)

내 의견이 완고한 함수형 CSS 옹호자들과 약간 다른 영역 중 하나는
유틸리티만으로 무언가를 구축해야 한다고 생각하지 않는다는 것입니다.

 

Tachyons같은 인기 있는 유틸리티 기반 프레임워크를 살펴보면
순수한 유틸리티를 이용해 일관된 버튼 스타일을 생성하는 것을 볼 수 있습니다.

<button class="f6 br3 ph3 pv2 white bg-purple hover-bg-light-purple">
  Button Text
</button>
이것을 분해해 보겠습니다.
  • f6: 글꼴 크기 스케일에서 여섯 번째 크기 사용(Tachyons의 경우 .875rem)
  • br3: border radius(.5rem) 스케일 에서 세 번째 크기를 사용합니다.
  • ph3: 수평 패딩(1rem)에 패딩 스케일의 세 번째 크기 사용
  • pv2: 수직 패딩(.5rem)에 패딩 스케일의 두 번째 크기 사용 흰색: 흰색 텍스트 사용
  • bg-purple: 보라색 배경 사용
  • hover-bg-light-purple: hover 시 밝은 보라색 배경 사용

동일한 클래스 조합을 가진 여러 버튼이 필요한 경우
Tachyons를 사용하여 권장되는 접근 방식은 CSS가 아닌 템플릿을 통해 추상화를 만드는 것입니다.

예를 들어 Vue.js를 사용하는 경우 다음과 같이 사용할 컴포넌트를 만들 수 있습니다.
<ui-button color="purple">Save</ui-button>
해당 컴포넌트는 아래와 유사하게 구현됩니다.
<template>
  <button class="f6 br3 ph3 pv2" :class="colorClasses">
    <slot></slot>
  </button>
</template>

<script>
export default {
  props: ['color'],
  computed: {
    colorClasses() {
      return {
        purple: 'white bg-purple hover-bg-light-purple',
        lightGray: 'mid-gray bg-light-gray hover-bg-light-silver',
        // ...
      }[this.color]
    }
  }
}
</script>

이것은 훌륭한 접근 방식이지만,

템플릿 기반 컴포넌트를 만드는 것보다 CSS 클래스를 활용하는 것이

더 실용적인 사용 사례가 많이 있다고 생각합니다.

 

사이트의 모든 작은 위젯을 템플릿화하는 것보다 (컴포넌트 별 변수에 클래스 조합 할당)
7가지 유틸리티를 묶는 새로운 .btn-purple 클래스를 만드는 것이 일반적으로 더 간단합니다. (css에 할당)

 

예시 : 카카오 엔터테인먼트에서 사용하는 빙식

https://fe-developers.kakaoent.com/2022/220303-tailwind-tips/

const ConfirmButton = (props) => {
  const { className, ...rest } = props;
  return (
    <button
      className={classnames(
        'bg-black text-red-400',
        className,
      )}
      {...rest}
    ></button>
  );
});

유틸리티 퍼스트 접근 방식

내가 CSS 유틸리티 퍼스트 접근 방식이라고 부르는 이유는
  • 유틸리티에서 가능한 모든 것을 구축하고
  • 반복 패턴이 나타날 때만 추출하려고 하기 때문입니다.

Less를 전처리기로 사용하는 경우 기존 클래스를 믹스인으로 사용할 수 있습니다.
btn-purple 컴포넌트를 만드는 데 약간의 다중 커서 마법사가 필요합니다.

기존 클래스를 mixin으로 사용하는 less

물론, 컴포넌트의 모든 각 선언을 전부 유틸리티에서 찾는 것이 항상 가능한 것은 아닙니다.
부모 위로 마우스를 가져갈 때 자식의 속성을 변경하는 것과 같은 요소 간의 복잡한 상호 작용은 유틸리티로만 수행하기 어렵습니다.
따라서 판단 후 최선이라고 생각되며, 더 간단하다고 생각되는 작업을 수행하세요

더 이상 성급한 추상화가 없습니다.

CSS에 대한 컴포넌트 우선 접근 방식을 취한다는 것은 재사용하지 않더라도 컴포넌트를 생성한다는 의미입니다.
이른 추상화는 스타일시트에서 많은 팽창과 복잡성의 원인입니다.

navbar를 예로 들어보겠습니다,
보통 앱에서 얼마나 많은 navbar를 위한 css를 중복 작성하나요?

 

저는 일반적으로  기본 레이아웃 파일에서 한 번만 수행합니다.
유틸리티로 먼저 빌드하고 걱정스러운 중복이 나타날 때만 컴포넌트를 추출한다면
아마도 navbar 컴포넌트를 추출할 필요가 없을 것입니다.

대신 nabvar를 다음과 같이 만들 수 있습니다.
<nav class="bg-brand py-4 flex-spaced">
  <div><!-- Logo goes here --></div>
  <div>
    <!-- Menu items go here -->
  </div>
</nav>
딱히 추출이 필요해 보이는 것은 없습니다.

단지 인라인 스타일에 불과하지 않나요?

이 접근 방식을 보고 HTML 요소의 스타일 태그에 필요한 속성을 추가하는 것과 같다고 생각하기 쉽지만
제 경험으로는 매우 다릅니다.

인라인 스타일을 사용하면 선택 가능한 값에 대한 제약이 없습니다.
한 태그는 font-size: 14px, 다른 태그는 font-size: 13px, 다른 태그는 font-size: .9em, 다른 태그는 font-size: .85rem일 수 있습니다.
모든 새 컴포넌트에 대해 새 CSS를 작성하는 것과 동일합니다.

유틸리티는 정해진 규칙 중에서 다음을 선택하도록 강요합니다.
  • text-sm인가요? text-x인가요?
  • text-dark-soft가 필요간가요? text-dark-faint가 필요한가요?
  • py-3 또는 py-4를 사용해야 하나요?

원하는 값을 맘대로 고를 수는 없습니다. 선별된 목록에서 선택해야 합니다.
380개의 텍스트 색상 대신 10 또는 12로 끝납니다.

유틸리티 퍼스트 CSS 시작하기

이 접근 방식이 흥미롭게 들린다면 다음 몇 가지 프레임워크를 확인해 볼 가치가 있습니다.
최근에 저는 PostCSS 기반 유틸리티 퍼스트 프레임워크인 Tailwind CSS를 출시했습니다.
위에서 설명한 아이디어를 중심으로 설계하였습니다.
  • 유틸리티성을 우선하여 작업하기
  • 반복되는 패턴에서 컴포넌트 추출

 

반응형