Tailwind CSS 사용기

이번에 새롭게 블로그를 만들면서 Tailwind CSS를 사용해봤습니다. Tailwind CSS는 익히 들어서 알고 있었지만 몇 가지 거부감에 그동안 사용하길 꺼렸습니다. 언뜻보기에 인라인 스타일과 다를 바 없어보이는 클래스들을 나열한다는게 어색했습니다. 게다가 기존의 CSS in CSSCSS in JS와 달리 이름없이 만들어진 컴포넌트들을 다른 사람이 이해할 수 있을지가 의문이었습니다.

그럼에도 사용하게된 이유

뒤늦게서야 제 스스로가 Utility-First에 대한 의문을 가지면서 동시에 프로젝트에 적용하고 있었다는걸 깨달았습니다.

import styled from 'styled-components';
export const Flex = styled.div`
display: flex;
`;
export const FlexCenter = styled(Flex)`
justify-content: center;
align-items: center;
`;
...
export const Ellipsis = styled.div`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;

이런 식으로 레이아웃을 지정하기도 하고,

const devices = {
mobile: '(max-width: 576px)',
tablet: '(max-width: 992px)',
desktop: '(max-width: 1200px)',
};
const theme = { devices };
export default theme;

이렇게 테마도 지정해서 사용했었습니다. Sass(SCSS)와 같은 CSS-in-CSS를 사용할 때도 마찬가지였습니다.

위와 같이 작성했던 유틸리티들이 편하다고 생각했던 이유는 무엇일까요?

  1. 작은 단위로 이루어져서 다른 컴포넌트에서 손쉽게 사용할 수 있다.
  2. 직관적인 네이밍으로 적용 후에 어떤 일이 일어나는 지 바로 알 수 있다.

이처럼 유틸리티들의 장점을 저도 모르게 느끼고 있었습니다. 동시에 Tailwind CSS에서 추구하는 Utility-First 컨셉에 공감하게 되면서 사용을 결정했습니다.

사용하면서 해소된 몇 가지 오해

CSS의 의미론적 요소는 어디에?

CSS 시맨틱

CSS의 경우입니다. 다양한 종류의 과일을 나타내기 위해서는 리스트 태그 li 가 있다고 가정해봅시다. div> ul> li 와 .fruits__item 둘 중 어떤 것이 어떤 DOM부분이 선택되었는지 잘 알려줄까요?

Semantics - MDN

CSS의 이름에 의미를 부여한다는 것은 당연한 것이었습니다.

// CSS
<style>
.fruits__item {
color: orange;
}
</style>
<li class="fruits__item">Orange</li>
// Tailwind CSS
<li class="text-orange">Orange</li>

위와 같이 CSS와 Tailwind CSS를 비교했을 때 어떤 역할을 하는 엘리먼트인지 직관적으로 알아볼 수 있는 것은 당연하게도 CSS입니다.

그렇다면 Tailwind CSS를 사용하면서 의미론적인 요소를 포기하게 되는 것일까요? 그렇지 않습니다. 이제는 컴포넌트가 그 역할을 대신해주고 있기 때문입니다.

FruitItem.jsx
function FruitItem() {
return <li class="text-orange">Orange</li>;
}

위와 같이 클래스 명이 아니라 컴포넌트의 이름만으로도 충분히 의미를 부여할 수 있습니다.

인라인 스타일과 무엇이 다를까?

인라인 스타일은 해당 스타일이 선언된 요소에만 적용됩니다.

아마도 인라인 스타일의 가장 큰 한계는 그들이 있는 요소에만 영향을 미칠 수 있다는 것입니다. 이것은 유틸리티 클래스와의 중요한 차이점입니다.

No, Utility Classes Aren't the Same As Inline Styles - Sarah Dayan

인라인 스타일은 적용한 엘리먼트에만 영향을 줍니다. 바꿔 말하면 적용한 엘리먼트 외 요소들과 상호작용 할 수 없습니다. 예를 들어 :hover와 같은 의사 클래스나 미디어 쿼리 등은 사용할 수 없게 됩니다.

반면에 Tailwind CSS는 PostCSS와 같은 전처리기를 거쳐서 스타일 시트를 생성하게 됩니다. 덕분에 인라인 스타일에서 할 수 없었던 것들을 할 수 있게 됩니다.

index.html
<li class="hover:text-orange">Orange</li>

위와 같이 호버 시에 글자 색이 바뀌는 행동을 유틸리티 클래스를 통해 할 수 있습니다.

소소한 팁

블로그에서 Tailwind CSS를 사용하면서 유용했던 몇 가지를 정리해보려고 합니다.

IntelliSense

공식 Tailwind CSS IntelliSense를 사용하면 Visual Studio Code에서 자동 완성, 문법 강조, 린팅 등의 기능을 이용할 수 있습니다. 제가 tailwind.config.ts에 정의해놨던 스타일들도 똑같이 지원해줘서 좋았습니다.

Prettier Plugin

공식 Prettier Plugin을 통해서 권장된 클래스 순서대로 자동 정렬을 지원합니다. 스타일링에 필요한 클래스를 쭉 나열해서 사용하면서 나중에 수정하게 됐을 때 동일한 순서로 정리해줘서 정말 편리했습니다. 덤으로 중복된 클래스를 적는 일도 사라졌습니다.

yarn add -D prettier-plugin-tailwindcss
.prettierrc.json
{
...,
"plugins": ["prettier-plugin-tailwindcss"]
}

Custom Styles

Tailwind CSS에서 충분히 많은 기본적인 스타일을 지원하지만 필요에 따라서 커스텀 스타일을 추가해야되는 경우가 있습니다. 저는 이 블로그를 개발하면서 다양한 색을 사용했는데 arbitrary values 없이 개발하고자 했기 때문에 모든 색을 등록해서 사용했습니다.

tailwind.config.ts
import type { Config } from 'tailwindcss';
export default {
theme: {
...,
extend: {
colors: {
light: {
DEFAULT: '#dddddd',
mute: '#595959',
link: '#004ec7',
background: '#eef1f5',
line: '#e5e7eb',
code: {
DEFAULT: '#ea580c',
background: '#fb923c1a',
},
},
dark: {
DEFAULT: '#191c1f',
mute: '#b3b3b3',
link: '#10b981',
background: '#222527',
line: '#3a3e42',
code: {
DEFAULT: '#a78bfa',
background: '#f3e8ff1a',
},
},
dimmed: '#00000066',
},
},
},
} satisfies Config;

위와 같이 nested object로 작성하면 -로 이어서 코드에서 사용할 수 있습니다.

<div class="text-light-mute"></div>

DEFAULT로 작성된 부분은 입력하지 않고도 사용할 수 있습니다.

// light: { DEAFULT: '#dddddd' }
<div class="text-light"></div>

이 뿐만 아니라 애니메이션도 등록해서 사용할 수 있습니다. 블로그 메인 페이지에서 글 리스트에 마우스를 호버하면 이모지가 회전하는 걸 구현하기 위해서 사용했습니다.

tailwind.config.ts
export default {
...,
animation: {
flip: 'flip 1s linear infinite',
},
keyframes: {
flip: {
'0%': { transform: 'rotateY(0)' },
'100%': { transform: 'rotateY(360deg)' },
},
},
} satisfies Config;
list.tsx
<div className="group-hover:animate-flip ...">{frontmatter.emoji}</div>

Dark Mode

다크 모드는 정말 손쉽게 추가할 수 있었습니다. 아무 설정을 건들지 않아도 클래스 앞에 dark:만 붙여주면 다크 모드에서 동작하는 스타일을 정의할 수 있었습니다.

footer.tsx
<footer className="dark:border-dark-line ...">ⓒ 2024. 김도현. All Rights Reserved.</footer>

기본적으로는 prefers-color-scheme에 맞춰서 동작하게 됩니다. 만약 클래스를 통해서 직접 조작하려면 darkMode: 'selector' 설정 한 줄만 추가해주면 됩니다.

tailwind.config.ts
import type { Config } from 'tailwindcss';
export default {
...,
darkMode: 'selector',
} satisfies Config;

개인적으로는 클래스를 통해서 조작하도록 설정하는 것이 좋았습니다. 코드 상에서 페이지 초기 진입 시에 prefers-color-scheme에 따라서 테마를 지정해주고, 헤더의 버튼을 통해서 사용자가 제어할 수 있게 했기 때문입니다.

Custom Utility

필요에 따라서 직접 커스텀 유틸리티 클래스를 정의할 수도 있습니다. 블로그에서 자주 사용되는 스타일은 따로 유틸리티 클래스로 만들어서 사용했습니다.

@apply를 통해 CSS 안에서 정의된 Tailwind CSS의 유틸리티 클래스를 사용할 수 있습니다.

global.css
@layer components {
.box {
@apply bg-light-background dark:bg-dark-background rounded-lg;
}
.link {
@apply text-light-link dark:text-dark-link;
}
.mute {
@apply text-light-mute dark:text-dark-mute;
}
}

느낀점

Tailwind CSS 사용 후기 글들을 읽어보면 공통적으로 나오는 말이 있습니다.

초반 러닝 커브가 있지만, 사용할수록 편리했다.

직접 사용해본 결과 전적으로 공감가는 말이었습니다. 초반에는 스타일을 지정해줄 때 하나하나 공식 문서에 검색했는데 적응한 이후로는 훨씬 빠르게 개발이 가능했습니다. 문법에 어느정도 일관성이 있기 때문에 나중에는 기억나지 않더라도 유추해서 작성하는 것도 가능했습니다.

styled-component를 사용하면서 이름 짓는데 많은 시간을 사용하면서 고통 받았는데 해방되는 기분도 들었습니다. 네이밍 컨벤션을 결정하거나, 이런 작은 컴포넌트까지 이름을 지어줘야하는지 고민하는데 드는 시간을 많이 줄인 것 같습니다.

또한 정의된 스타일을 통해 개발하기 때문에 일관된 스타일을 유지할 수 있었습니다. 게다가 스타일링을 위해서 파일이나 커서를 옮겨야하는데 드는 컨텍스트 스위칭 시간도 없어서 편리했습니다.

앞서 Tailwind CSS의 좋은 점만 나열하게 된 것 같은데 꼭 그런 것은 아닙니다. 동적 스타일링을 하기 힘들기 때문에 외부 플러그인에 도움을 받아야하는 경우가 생깁니다. 아쉽게도 블로그를 개발하면서는 동적 스타일링을 할 경우가 많지 않았습니다. 하지만 나중에 규모가 큰 프로젝트에서 사용하게 될 때는 지나칠 수 없는 문제가 될 것 같습니다.

그리고 개인 프로젝트 외에 현업에서 사용해본 적은 없지만 디자인 시스템이 없거나 있어도 애매한 부분에는 arbitrary values가 점점 쌓이면서 금새 복잡해질 것 같다는 생각이 듭니다.

이와 관련해서는 FE개발그룹에서는 Tailwind CSS를 왜 도입했고, 어떻게 사용했을까? 라는 좋은 아티클이 있어서 같이 남겨놓습니다.

끝으로 많은 선입견을 갖고 있던 Tailwind CSS를 직접 사용해보면서, 기존에 개발하면서 가지고 있던 불편함들이 다양한 방법으로 해소되는 경험을 할 수 있었습니다. 세상에 완벽한 도구란 없는 것 같고, 필요한 도구를 적합한 장소에 사용할 줄 아는 개발자가 되어야겠습니다.

참조