본문으로 건너뛰기
김도현

Tailwind CSS 사용기

블로그에 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

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

CSS 안에서 정의된 Tailwind CSS의 유틸리티 클래스를 쓰고 싶다면 @apply를 이용하면 된다.

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를 직접 사용해봤다. 기존에 개발하면서 느끼던 불편함들이 다양한 방법으로 해소되는 경험이었다. 세상에 완벽한 도구란 없으니 필요한 도구를 적합한 장소에 사용할 줄 아는 개발자가 되어야겠다.

참조

관련 글