Tailwind CSS는 Utility-First 컨셉을 가진 CSS 프레임워크. 부트스트랩과 비슷하게 m-1
, flex
와 같이 미리 세팅된 유틸리티 클래스를 활용하는 방식으로 HTML 코드 내에서 스타일링을 할 수 있음.
CSS 프레임워크로 인라인 스타일을 사용하는 것만큼 쉽고 빠르게 스타일링을 할 수 있으면서도 디자인 시스템만큼이나 일관된 디자인을 가능하게 해줌.
styled-components 등과 같이 작은 스타일 변경에도 컴포넌트를 만들어야 하는 번거로움에 지쳤거나 매번 클래스명을 고민하느라 힘든 분들이 참고하면 좋음.
React는 사용했을 때는 Vue처럼 컴포넌트로 스코프가 제한된 스타일 템플릿을 제공하면서도 styled-components처럼 CSS 코드에서 JS 코드를 활용할 수 있는 styled-jsx를 사용함.
Vue는 기본적으로 컴포넌트로 스코프가 제한된 스타일 템플릿을 제공하기에 스타일링 방식에 대해 크게 고민할 필요가 없음.
Vue 스타일 템플릿이나 styled-jsx 모두 HTML 코드와 CSS 코드를 오가는 게 번거롭고, 수정할 때마다 클래스명을 검색해서 찾는게 불편함.
Utility-First 컨셉은 Tailwind CSS의 메인 컨셉이자 가장 큰 장점. 이 글에서도 볼 수 있듯 Utility-First 덕에 매우 쉽고 빠르게 원하는 디자인을 개발할 수 있게 됨.
스타일 코드도 HTML 코드 안에 있기 때문에 HTML와 CSS 파일을 별도로 관리할 필요가 없음. 그 덕분에 HTML-CSS를 왔다갔다하며 시간을 쓰거나 위의 이미지처럼 화면을 분할해서 사용하지 않아도 됨. 또 원하는 태그의 스타일을 변경하기 위해 클래스명을 검색해가며 일일이 필요한 CSS 코드를 찾을 수고도 사라짐.
또 마크업을 하다 보면 반드시 겪게 되는 고퉁 중에 하나는 랩핑하는 태그의 클래스명을 고민하는 일이다. BEM, OOCSS 등의 방법론이 나올 정도로 클래스명 짓는 일은 까다롭다. 어렵사리 규칙을 잘 정해도 그리 효율적이지도 않은 경우가 많다. Tailwind CSS를 사용하면 랩핑 태그의 클래스명을 사용할 일이 거의 없으므로 container
, wrapper
, inner-wrapper
와 같은 클래스명을 고민하지 않아도 됨.
기존에는 페이지 단위로 UI 작업을 진행. A라는 페이지를 모두 만든 후 B라는 페이지 작업을 진행했는데 이때 A 페이지와 B 페이지에 비슷해 보이는 컴포넌트를 발견해도 웬만해서는 이를 공통 컴포넌트로 합칠 생각을 하지 못했음. 디자인팀과 별도로 합의된 시스템이 없다 보니 두 컴포넌트가 공통적인 요소를 가지고 있으므로 공통 컴포넌트로 합치기에 적절하다는 판단을 내릴 근거가 부족했기 때문. 이로 인해 비슷한 UI를 가진 컴포넌트들이라도 공통 컴포넌트로 만들기보다는 별도의 컴포넌트로 만드는 상황이 자주 발생함.
이런 상황이 반복되다 보니 비슷한 컴포넌트들이 이름만 바뀐 채 여러 개 만들어졌고, 이런 컴포넌트가 기하급수적으로 늘면서 각 컴포넌트의 용도를 파악하는 게 어려워져 결국 요구사항이 바뀌더라도 선뜻 손대기 힘든 지경까지 이르렀음.
기존에는 디자인에서 사용하는 컬러나 폰트 등에 별도의 네이밍을 붙여 관리하지 않음. 이러다 보니 만약 디자인 팀에서 폰트 사이즈가 14px로 적용된 곳들을 모두 16px로 변경해주세요라는 요청을 한다면, 개발자가 이 작업을 처리할 수 있는 유일한 방법은 14px로 폰트 사이즈가 적용된 부분들을 모두 탐색한 후 일일이 16px로 변경해주는 것이. 만약 14px 폰트 사이즈가 적용된 폰트 자체에 font-small과 같은 별도의 네이밍을 붙여 관리했다면, 단순히 해당 네이밍을 가진 폰트의 폰트 사이즈 정의를 14px에서 16px로 변경하기만 하면 되는 - 딱 한 군데만 수정하면 되는 - 매우 간단한 문제였을 것. 작업이 간단하면 간단할수록 개발자가 실수할 확률도 줄어듦.
모든 곳에서 동일한 색상이나 사이즈, 간격 등의 유틸리티 클래스를 사용하므로 일관된 스타일로 구현하기가 수월함.
Tailwind CSS는 다른 프레임워크들에 비해 기본 스타일 값을 디테일한 부분까지 쉽게 커스텀이 가능함. 커스텀을 할 때 기본 스타일 값을 수정하는 방식이기에 디자인 일관성도 해치지 않음. 덕분에 디자인 시스템(클래스명을 설정하는 것만으로도 디자인 시스템의 색상이나 간격, 사이즈 등을 빠르게 반영할 수 있음.)이나 다크 모드 구현도 간편함.
각 CSS 요소 수준의 유틸리티 클래스를 제공하기 때문에 세밀하게 원하는 디자인을 구현할 수 있음.
로우 레벨의 스타일을 제공한다는 것은 거의 모든 스타일의 유틸리티 클래스를 학습해야 한다는 의미와 같음. 이러한 불편을 해소하기 위해 Intelli Sense 플러그인을 제공함. 미리보기, 자동완성, 신택스 하이라이팅, 린팅을 지원하기 때문에 조금만 익숙해지면 금방 문서 없이 개발할 수 있음.
Tailwind CSS는 JavaScript 코드와 완전히 분리되어 있음. 그러므로 프로젝트 진행 도중 JavaScript 프레임워크를 변경하여도 큰 추가 작업 없이 기존의 HTML 코드를 그대로 쓸 수 있음.
디자인 시스템을 위한 부가코드가 실제 기능 코드를 침범하지 않음.
( 적어도 실제 컴포넌트 내부 구현을 침범 안함. )
공통된 네이밍의 부재 - 의 경우 디자인 시스템에서 컬러나 폰트들에 대한 공통된 네이밍을 정의하더라도 이를 그대로 코드에 적용하기에 Emotion CSS로는 다소 불편한 점들이 있었음.
예를 들어, 디자인 팀에서 카카오페이지 웹 사이트에서 사용될 일부 폰트들에 대해 아래와 같이 네이밍을 붙여 관리한다고 가정.
Emotion CSS를 통해 이 네이밍을 코드상에서 그대로 활용하려면 당장 머릿속에서 떠오르는 방법은 텍스트를 표현하기 위한 별도의 컴포넌트를 정의하는 것. StyledText 컴포넌트를 정의하고 여기에 type 속성을 주입함으로써, 우리는 디자인 시스템에 정의된 네이밍대로 텍스트에 폰트를 명시할 수 있게 됨.
import { css } from '@emotion/react';
enum FontType {
Large1B,
Large1R,
...
}
const fontMap = {
[FontType.Large1B]: css`
font-size: 24px;
font-weight: bold;
line-height: 30;
`,
[FontType.Large1R]: css`
font-size: 24px;
font-weight: normal;
line-height: 30;
`,
...
};
export function StyledText({ type, text }: { type: FontType; text: string }) {
return <span css={...fontMap[type]}>{text}</span>;
}
// 디자인 시스템에서 정의한 Large1_B라는 네이밍을 거의 그대로 사용 가능
<StyledText type={FontType.Large1R} text="안녕하세요"></StyledText>;
하지만 별도의 컴포넌트로 정의하기 애매한 경우는 어떻게 해야 할까요? 예를 들어, 디자인 팀에서 백그라운드 컬러로 사용될 일부 색상들에 대해 아래와 같은 네이밍을 붙여 관리하기로 했다고 가정.
백그라운드 컬러의 네이밍을 위해 별도의 컴포넌트를 정의하는 것은 너무 과하게 느껴짐. 설령 컴포넌트를 정의한다고 해도 사용하기에 불편할 것 같음. 이런 경우는 Emotion CSS에서 제공하는 Theming 기능을 사용하면 적절할 듯 함.
import { useState } from 'react';
import { ThemeProvider } from '@emotion/react'
const theme = {
colors: {
bgA10: '#000000',
bgA20: '#181818',
...
}
}
function App() {
return (
<ThemeProvider theme={theme}>
<div css={theme => ({ background: theme.colors.bgA10 })}>백그라운드</div>
</ThemeProvider>
);
}
이처럼 Emotion CSS를 통해 디자인 시스템에 정의된 백그라운드 컬러 네이밍을 코드에 적용할 수 있음. 위에서 언급한 폰트의 경우에도 이 Theming 기능을 사용하면 별도의 컴포넌트를 정의하지 않아도 될 것 같음.
하지만 단순히 디자인 가이드에서 붙여준 이름을 그대로 사용하기 위해 너무 많은 코드가 추가되는 것 같음. ThemeProvider라는 녀석으로 우리 코드를 감싸야 하고, 더군다나 부모 ThemeProvider에서 주입해준 theme을 자식 컴포넌트에서 사용하려면 useTheme 훅을 사용하거나 다른 별도의 처리가 필요함.
function children() {
const theme = useTheme();
// useTheme을 사용하면 부모의 ThemeProvider에서 주입해준 theme을 가져올 수 있습니다.
return <div css={{ background: theme.colors.bgA20 }}></div>;
}
이러한 방식은 디자인 시스템을 위해 추가한 코드가 실제 ‘기능 코드’를 침범하게 되어 서로간 의존성이 높아지게 됨. 또한 네이밍이 붙은 폰트나 컬러 등을 사용하려면 항상 theme이라는 녀석의 존재 유무를 신경 써야 하기 때문에 보일러플레이트가 많아진다는 단점이 있음.
디자인 코드가 기능 코드를 침범하는 경우는 theme과 같은 기능을 사용할 때만 한정되지 않음. Emotion을 통해 특정 HTML 요소에 디자인을 명시할 때는 인라인 스타일을 주입해주는 것과 유사하게 CSS 속성 명과 값을 그대로 명시해줘야 함. 만약 복잡한 스타일이 추가되어야 하는 요소라면 Emotion CSS를 사용하는 경우 아래와 같은 긴 스타일 코드가 추가됨.
<span
css={css`
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.5;
word-break: break-all;
white-space: no-wrap;
`}
>
{text}
</span>
이러한 CSS-in-JS 방식을 사용하면 HTML 요소에 어떤 디자인이 적용되어 있는지 직관적으로 확인할 수 있다는 장점이 있지만, 컴포넌트 내부에 이런 css 코드가 인라인으로 많이 포함된다면 컴포넌트의 가독성을 심하게 해치게 됨. 당장 위에서 살펴본 div 하나만 해도 css를 위해 총 8줄의 코드가 추가됐는데, 컴포넌트에 HTML 요소가 하나만 있는건 아니죠. 한 컴포넌트에 이런 형태의 HTML 요소가 10개만 있어도 css 코드가 80줄이나 추가되게 됨. 디자인 코드로 한 컴포넌트에 이렇게 많이 들어가면 실제 로직을 파악하는 데 문제가 생길 수 있음.
카카오페이지 웹을 새로 개편할 때는 Emotion CSS를 사용함에 따라 발생하는 이런 문제들을 그대로 답습하면 안 된다고 생각함. 우리가 극복해야 했던 문제들은 다음과 같음.
이 문제들을 해결하려면 다음과 같은 전제가 보장되어야 함.
Tailwind CSS라는 라이브러리가 이 전제들을 만족시키는 가장 적절한 해결책이라고 생각했고, 따라서 문제 해결을 위한 도구로 프로젝트에 tailwind를 도입하도록 결정. tailwind를 사용하면 디자인 시스템에서 정의한 네이밍을 사용하기 위해 컴포넌트 내에 어떠한 코드도 추가할 필요가 없으며, CSS-in-JS 방식과 유사하게 HTML 요소 자체에 디자인을 명시하기 때문에 기능 코드와 디자인 코드의 파편화를 막되 유틸리티(utility)라는 개념을 통해 가독성을 해치는 부분을 최소화할 수 있음.
Tailwind CSS를 사용한다면 위에서 예시로 든 StyledText 요소를 다음과 같이 간단하게 정의할 수 있음. 스타일을 정의하기 위해 기본적으로 제공되는 class 속성을 사용했으며, 스타일 코드가 요소에 직접 존재하지만 컴포넌트의 가독성을 해칠만큼 길지는 않음.
<span class={"line-clamp-2 truncate break-all leading-normal"}>{text}</span>
지금까지 ‘왜’ tailwind를 선택했는지에 대해 길게 설명했으니, 저희 프로젝트에서 tailwind를 ‘어떻게’ 사용했는지에 대한 얘기를 하고자 함.
먼저 Twin.Macro 라이브러리에 대해 간단히 소개하려고 함. 저희 팀은 tailwind를 직접 사용하는 대신 Twin.Macro라는 라이브러리를 활용해 디자인 코드를 작성하고 있음.
Twin.Macro라는 라이브러리는 기본적으로 Tailwind CSS에 유연함을 추가해주는 라이브러리. tailwind 라이브러리만 사용한다면 HTML 요소에 스타일을 명시하는 방법이 다음 두 가지로 제한됨.
function ExampleDiv({ width, height }) {
return <div>안녕하세요</div>;
}
div 요소에 인자로 받은 width와 height를 스타일로 넣어주고 싶은데, tailwind로는 외부에서 전달해준 가변적인 인자를 div의 스타일로 주입해줄 방법이 없음. tailwind는 유틸리티에 임의 값을 넣을 수 있는 Arbitrary value support
라는 기능을 제공하지만, 20px과 같은 단위를 직접 넣을 수 있을 뿐 다음과 같이 변수를 넣어줄 수는 없음.
function ExampleDiv({ width, height }) {
return <div class=`bg-black w-[${width}px] h-[${height}px]`>안녕하세요</div>; // X
}
이런 경우에는 인라인 스타일 이외의 선택지가 없기 때문에 울며 겨자 먹기로 인라인 스타일을 사용해야 하지만, 요소 자체에 스타일이 노출되는 인라인 스타일의 사용은 최대한 배제하고 싶음.
Twin.Macro는 우리가 쫒아낸 Emotion CSS를 다시 데려와, Tailwind CSS와 ‘함께’ 사용할 수 있게 만들어줌. 즉, Tailwind CSS와 Emotion CSS를 동시에 ‘유연하게’ 사용할 수 있게 해줍니다. Twin.Macro를 사용한다면 우리가 고민하고 있던 문제를 다음과 같이 단숨에 해결해줌.
import tw from "twin.macro";
function ExampleDiv({ width, height }) {
return (
<div
css={[
tw`bg-black`,
{
width: width,
height: height,
},
]}
>
안녕하세요
</div>
);
}
css 속성을 사용하는 걸 보니 어딘가 익숙해 보임. 우리가 Emotion CSS를 사용할 때 이런 문법을 사용함. css 속성에는 직접 스타일을 주입해줄 수 있기 때문에 가변 값인 width와 height 값을 사용할 수 있고, twin.macro 라이브러리로부터 임포트 해온 tw 함수 (이름은 아무거나 붙여줘도 됩니다. 개인적으로는 tw가 가장 적절한 것 같습니다)를 통해 tailwind 유틸리티를 직접 명시해주는 것도 가능함. class 속성에 유틸리티를 따로 넣어주는 대신에.
Twin.Macro는 이렇게 tailwind와 Emotion CSS를 함께 사용할 수 있는 방법을 제공해줌으로써, 개발자가 상황에 따라 최적의 방법을 선택해 디자인 코드를 작성할 수 있도록 도와줌. Twin.Macro는 또한 스타일 우선순위 문제를 해결해주는 등 개발할 때 불필요하게 신경 쓸 부분을 줄여주는 기능들도 제공해주기 때문에 사용하지 않을 이유가 없다고 판단함. (이 부분은 코제가 작성하신 이 글에 상세히 설명되어 있음)
카카오페이지 웹을 개편하면서 tailwind를 그대로 사용하기 보다 Twin.Macro 라이브러리를 적극적으로 사용했기 때문에, 이후 설명은 Twin.Macro 라이브러리를 사용한다는 가정하에 하도록 함.
tailwind는 기본적으로 많은 유틸리티를 제공하지만, 기본으로 제공되는 유틸리티로는 부족할 때가 종종 있음. 물론 Emotion CSS 문법을 사용해도 되지만, Emotion CSS를 사용하면 스타일 코드가 길어지므로 기왕이면 유틸리티를 통해 한 줄로 해결하고 싶음.
위에서 StyledText 코드를 tailwind 코드로 변환한 예시를 보여드릴 때 은근슬쩍 사용한 line-clamp-2 유틸리티도 사실 기본 제공 유틸리티가 아님. 이 유틸리티는 tailwind에서 제공하는 @tailwindcss/line-clamp 라이브러리를 설치한 후, 해당 라이브러리를 tailwind 플러그인으로 사용하도록 설정해야 사용할 수 있음.
플러그인은 Tailwind CSS에서 제공하는 기본 유틸리티에서 벗어나, 완전히 새로운 유틸리티를 정의할 수 있도록 tailwind가 제공해주는 기능. 하지만 @tailwindcss/line-clamp 처럼 tailwind에서 직접 제공해주는 플러그인은 그다지 많지 않음. 목마른 사람이 직접 우물을 판다고, 우리가 원하는 기능을 정확하게 제공하는 유틸리티를 추가하기 위해서는 커스텀 플러그인을 직접 구현해 적용해야 함.
커스텀 플러그인은 tailwindcss 라이브러리에서 제공하는 tailwindcss/plugin 함수를 사용해 정의할 수 있음. 이 함수를 통해 커스텀 플러그인을 정의할 방법이 매우 다양하기 때문에 이 글에서 모두 소개하기는 힘들고, 매우 간단한 커스텀 플러그인을 정의하는 예시 하나만 보여드리는 정도로 마무리.
특정 요소가 스크롤은 가능하되, 스크롤바는 보여주지 않도록 스타일을 추가하고 싶음. 웹서핑을 해보면 이 작업이 단순히 한 줄의 코드로 끝나지 않는다는 사실을 발견하실 수 있음. 브라우저마다 스크롤바의 디자인을 제어할 수 있는 css 속성 명이 다르기 때문임. 대부분의 메이저 브라우저 환경에 대해 스크롤바를 숨기기 위해서는 대략 다음과 같은 속성들을 요소에 일괄 추가해야함.
{
/* Firefox */
'scrollbar-width': 'none',
/* IE */
'-ms-overflow-style': 'none',
/* Safari and Chrome */
'&::-webkit-scrollbar': {
display: 'none',
},
}
특정 요소의 스크롤바를 숨기고 싶을 때마다 이 스타일들을 요소에 계속 반복적으로 주입해주는 건 너무 번거로운 일. 따라서 한 줄의 코드(유틸리티)로 스크롤바를 숨기는 스타일을 컴포넌트에 적용하기 위해, tailwind 커스텀 플러그인을 정의하려고 함.
js 파일을 하나 생성. 이름은 우리가 추가하려는 유틸리티 명과 동일한 scrollbar-hide.js로 정함 (반드시 동일할 필요는 없습니다). 그리고 이 파일에 tailwindcss/plugin 함수를 import.
// scrollbar-hide.js
const plugin = require("tailwindcss/plugin");
우리의 최종 목적은 plugin 함수를 통해 새로운 ‘tailwind 유틸리티’를 추가하는 것. plugin 함수는 이를 위해 addUtilities 라는 함수를 내부적으로 제공. 아래는 이 함수를 활용해 scrollbar-hide라는 이름의 커스텀 유틸리티를 정의한 코드.
// scrollbar-hide.js
const plugin = require("tailwindcss/plugin");
const scrollbarHide = plugin(function ({ addUtilities }) {
addUtilities({
// . + 유틸리티 이름
".scrollbar-hide": {
"scrollbar-width": "none",
"-ms-overflow-style": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});
module.exports = scrollbarHide;
이후 tailwind 설정 파일에 들어가서, plugins 속성에 scrollbar-hide 함수를 배열 인자로 넣어주면 커스텀 플러그인이 정상적으로 적용됨.
// tailwind.config.js
...
plugins: [
require('./plugins/scrollbar-hide'),
]
이로써 우리는 스크롤은 가능하되 스크롤바를 숨기고 싶은 컴포넌트에 다음과 같이 한 줄만 추가해주면 우리가 원하는 바를 이룰 수 있게 됨.
<div css={tw`scrollbar-hide`}>
// 혹은
<div class="scrollbar-hide">
현재 FE개발그룹에서는 직접 정의한 다양한 커스텀 플러그인을 사용해, 자주 사용되는 복잡한 스타일들을 한 줄로 압축해 사용하고 있음.
웹사이트를 개발할 때 해상도에 따라 UI를 다르게 보여줘야 하는 경우가 있음. 예를 들어, PC환경을 기준으로 UI를 만들었으나 화면이 작은 모바일 환경에서는 UI가 찌그러질 수 있음. 이런 경우에는 보통 특정 해상도 아래에서 아이템의 크기를 작게 보여주는 등의 다해상도 대응 코드를 추가하곤 함.
매우 고맙게도 tailwind는 다해상도 대응을 위한 자체적인 중단점(breakpoint)들을 제공하고 있음. 기본적으로는 다음과 같은 중단점들을 지원.
'sm': '640px' // @media (min-width: 640px)
'md': '768px' // @media (min-width: 768px)
'lg': '1024px' // @media (min-width: 1024px)
'xl': '1280px' // @media (min-width: 1280px)
'2xl': '1536px' // @media (min-width: 1536px)
다만 기본적으로 제공하는 중단점들의 경우 네이밍이 sm, md 등으로 정확히 어떤 경우를 의미하는지 너무 애매모호하며, px 기준도 저희가 필요로하는 다해상도 대응과 일치하지 않음. 따라서, 저희 팀에서는 다음과 같은 중단점들을 직접 정의해 사용 중. 각각의 중단점들이 정확히 어떤 상황에 대한 중단점인지를 이름을 통해 쉽게 파악할 수 있도록 구현.
mobile: '360px' // @media (min-width: 360px)
foldable: '523px' // @media (min-width: 523px)
tablet: '768px' // @media (min-width: 768px)
'under-foldable': { max: '522px' } // @media (max-width: 522px)
'under-tablet': { max: '767px' } // @media (max-width: 767px)
'under-mobile': { max: '359px' } // @media (max-width: 359px)
이렇게 중단점들을 정의하고 나면, 다음과 같이 손쉽게 다해상도 코드를 적용할 수 있음.
// 768px 이상에서만 overflow-hidden 적용, bg-black은 모든 경우에 적용
<div css={tw`tablet:overflow-hidden bg-black`} />
// 768px 이상에서만 overflow-hidden 및 bg-black 적용
<div css={tw`tablet:(overflow-hidden bg-black)`} />
중단점(여기서는 tablet) 뒤에 괄호를 붙이지 않으면 바로 뒤에 오는 유틸리티 하나만 중단점이 적용되며, 괄호를 붙인다면 괄호 안에 있는 모든 유틸리티에 중단점이 적용되게 됨. tailwind를 사용하면 이처럼 다해상도 대응을 위해 복잡한 코드를 넣을 필요 없이, 중단점만을 사용해 매우 간단하게 대응할 수 있게 됨.
우리가 흔히 사용하는 px 단위는 모니터 해상도에 따라 결정되기 때문에, 동일 해상도에서는 항상 동일한 크기로 보임. 이런 px 단위는 접근성 문제를 야기할 수 있음.
대부분의 브라우저는 디폴트 폰트 사이즈로 16px의 폰트 크기를 가지지만, 브라우저의 접근성 설정에서 디폴트 폰트 크기를 바꿀 수 있는 기능을 제공함. 하지만 유저가 웹사이트의 글씨를 더 크게 보고 싶어서 이 설정을 조정한다고 해도 웹사이트의 모든 단위가 px로 설정되어 있다면 아무것도 바뀌지 않게 됨. 디폴트 폰트 사이즈가 변경된다고 해서 우리가 직접 요소들에 넣어준 px 크기가 변경되지 않기 때문.
px과 달리 em 및 rem 단위는 ‘현재 폰트 사이즈’를 기준으로 크기가 결정되기 때문에, 동일한 해상도에서도 현재 폰트 크기에 따라 크기가 다르게 보일 수 있음.
저희 팀에서 집중했던 부분은 rem. rem은 최상위 엘리먼트(html)에 지정된 폰트 사이즈를 기준으로 크기가 결정되는데, 최상위 엘리먼트에 폰트 사이즈를 임의로 명시하지 않으면 브라우저 기본값 (보통 16px)을 기준으로 크기가 결정됨 (1rem = 16px). rem을 사용한다면 유저가 브라우저 설정의 ‘접근성’ 기능을 통해 디폴트 텍스트 크기를 조정했을 때, 그에 따라 rem의 크기도 함께 변경되기 때문에 유저가 의도한 동작을 지원할 수 있게 됨.
하지만 항상 px 단위를 사용해온 개발자 입장에서 선뜻 모든 곳에서 px 대신 rem 단위를 사용하도록 바꾸는 것은 어려운 일. 무엇보다 디자인 팀에서 페이지를 디자인할 때, px 단위가 아니라 rem 단위로 제작하도록 강제한다는 것 자체가 어려운 일이기도 함. 디자인 도구에서 px 단위만 지원하는 경우도 있음.
즉, 저희 팀이 유저 접근성 향상을 위해 rem 단위를 사용하려면 다음과 같은 전제조건이 필요함.
“개발할 때는 디자인에서 명시된 px 단위로 디자인하되, 실제 페이지에는 rem 단위로 적용되어야 합니다.”
저희 팀은 tailwind의 힘을 빌려 이 전제조건을 충족시키는 방법을 찾아냄, 바로 pxr이라는 단위를 새로 만들어 도입한 것. 이 pxr 단위를 사용하면 디자인에 명시된 px 값을 그대로 사용할 수 있으며 (16pxr = 16px), 실제 페이지에는 rem 단위로 치환되어 적용되게 됨 (16pxr = 1rem).
tailwind에서 기본적으로 크기를 결정하는 설정은 spacing에 존재. pxr이라는 새로운 단위를 tailwind에서 사용할 수 있도록 다음과 같은 설정을 tailwind 설정 파일에 추가.
// tailwind.config.js
const pxToRem = (px, base = 16) => `${px / base}rem`;
module.export = {
...
theme: {
...
spacing: {
...range(1, 100).reduce((acc, px) => {
acc[`${px}pxr`] = pxToRem(px);
return acc;
}, {}),
}
}
}
위 코드는 디폴트 폰트 사이즈를 16px로 가정하고, 1rem = 16px이라는 공식에 따라 1~100px 까지의 단위를 pxr로 정의한 코드. 즉 1pxr 라는 단위는 1/16rem이라는 값으로 맵핑되며, 16pxr은 1rem이라는 값으로 맵핑됨.
지금까지 px 단위를 사용해왔던 것과 동일하게 다음과 같이 사용할 수 있음. 아래 div는 결과적으로 1rem의 너비와 1rem의 높이를 가지게 됨. 실제 요소에는 rem으로 값이 할당되기 때문에, 유저가 브라우저의 디폴트 폰트 사이즈를 조정하면 그에 따라 div의 크기가 가변적으로 크기가 변경됨.
<div css={tw`w-16pxr h-16pxr`} />
pxr이라는 단위는 저희 팀에서 실험적으로 도입한 단위이며, 이 글에서 pxr 단위를 꼭 사용해야 한다고 말씀드리고 싶은 것은 아님. 단지 tailwind를 사용하면 여러분이 원하시는 그 어떤 단위도 실제로 정의해 코드 상에서 사용할 수 있다는 것을 소개.
많은 웹사이트에서 다크모드를 지원하는 추세. 웹사이트뿐만이 아님. 많은 모바일 OS에서 자체적으로 다크모드를 지원하기도 함.
tailwind는 OS의 다크모드 설정에 따라 다른 디자인을 적용할 수 있는 기능을 자체적으로 제공함. 즉 다음과 같은 div 요소가 있다면,
<div css={tw`w-200pxr h-200pxr bg-black dark:bg-white`}>
OS의 다크모드가 켜져있을 때는 흰색 배경색을 가지고 다크모드가 꺼져있을 때는 검은색 배경색을 가지게 됩니다. dark 중단점의 사용법은 다해상도 중단점을 사용할 때와 동일합니다.
하지만 OS 기본 다크모드 지원에만 기대어 웹에서 다크모드를 지원하는 것은 너무 부족해 보입니다. 다크모드를 지원하지 않는 OS가 있을 수도 있고, 모바일이 아니라 PC 환경에서는 OS에서 제공하는 다크모드를 켜는 방법을 알기도 쉽지 않습니다. (적어도 저는 PC를 사용하면서 OS 다크모드를 사용해 본 경험이 없습니다)
이러한 이유로 대부분의 웹사이트에서는 특정 버튼을 눌러서 유저가 다크모드를 자유롭게 활성화/비활성화 시킬 수 있는 기능을 제공합니다. 하지만 tailwind에서 기본적으로 제공하는 컬러모드 지원으로는 이를 구현하기 힘들어 보입니다.
tailwind에서는 OS의 다크모드 지원에 의존하지 않고 개발자가 직접 다크모드 기능을 제어할 수 있도록 하는 옵션을 제공해줌. tailwind 설정 파일에 다음 코드를 추가하면,
// tailwind.config.js
module.exports = {
darkMode: "class",
// ...
};
우리는 클래스를 통해 다크모드를 직접 제어할 수 있게 됨. 여기서 클래스로 제어한다는 의미는, HTML 트리 최상단 (즉 html, body 등등)에 다음과 같은 클래스가 존재한다면
<html class="dark">
다크모드로 인식되어 dark 중단점이 동작하게 되고, 해당 클래스가 없다면 라이트모드로 인식되어 dark 중단점이 동작하지 않는다는 의미. 유저가 라이트모드 상태에서 특정 버튼을 눌렀을 때에는 트리 최상단 요소에 class="dark"를 추가해주고, 다크모드 상태에서 눌렀을 때에는 해당 클래스를 제거해주도록 코드를 구현한다면 훌륭한 다크모드 토클 버튼을 만들 수 있게 됨.
VSCode 상에서 tailwind를 사용하면서 가장 좋았던 것은 IntelliSense 경험이 매우 좋았다는 점. 심지어 Twin.Macro도 IntelliSense를 지원!
tailwind에서 제공하는 디폴트 유틸리티에 대한 자동완성만 지원하는 것이 아니라, 개발자가 직접 정의한 유틸리티 및 기능들에 대해서도 모두 자동완성을 지원한다는 점이 놀라움. Twin.Macro의 IntelliSense는 여기 에서 다운로드받거나, VSCode 마켓플레이스에 Tailwind Twin IntelliSense를 검색하신 후 다운로드받으실 수 있음.
아래 영상에서 보실 수 있듯이 직접 정의한 pxr 단위와 scrollbar-hide 유틸리티에 대한 자동완성도 완벽하게 지원되는 것을 확인하실 수 있음.
또한 특정 유틸리티 위에 마우스를 올리면, 이 유틸리티가 어떻게 실제 css로 변환되어 적용될지에 대한 상세 설명을 볼 수도 있음. 여러분이 이전에 추가한 유틸리티가 뭔지 기억할 필요가 없음. 마우스만 올리면 뭔지 알 수 있음.
아무래도 가장 큰 단점은 코드가 못생겼다는 점. 아마 Tailwind CSS에서 가장 큰 진입장벽이 아닐까 싶다 😂 직관적이라고 할 수도 있겠지만 예쁘다고 말하기는 어려움.
초반에는 각 스타일의 클래스명을 익히느라 개발하는 내내 문서를 참고해야 하는 번거로움이 있음. 그래도 대부분의 클래스명이 기존 CSS 속성이나 속성값과 비슷한 경우가 많고 자동완성을 지원하는 Intelli Sense 플러그인이 있어서 금방 익숙해지기는 함.
클래스명을 분기 처리하여 동적으로 스타일링을 설정할 수는 있지만 styled-components와 같이 JavaScript 변수 값에 따라 가로 길이를 설정하는 등의 구현은 가능하기는 하지만 무척 번거로운 설정이 필요함. 그러나 나는 이렇게 특정 변수값을 활용하여 스타일링을 하는 경우가 일관된 디자인을 해치는 경우가 많기에 지양하는 편. 실제로 블로그를 구현할 때 이러한 설정이 필요한 경우가 한 번도 없었음.
Tailwind CSS의 단점을 꼽을 때 HTML와 CSS의 관심사 분리가 이루어지지 않았다고 언급되는 경우가 있음. 그래서 단점의 한 꼭지로 넣었긴 했으나 개인적으로 HTML와 CSS를 다른 관심사로 분리할 수 있는가는 의문.
Tailwind CSS Main Concept로 Class명을 별도로 정의하지 않고 제공하는 Utility-Class를 사용 하는 특징.
margin, padding, color, position 등 다양한 CSS 속성들을 사용하기 편하도록 미리 정의해둔 클래스들로 기본 컴포넌트 클래스들과 결합해 사용하는 클래스.
사용하는 프레임워크나 도구에 따라 설치 방법이 조금씩 다름. 공식 문서에는 Next.js, Vue3, Laravel, Nuxt.js, Create React App, Gatsby 설치 방법이 나와있음.
기본적으로 PostCSS 플러그인으로 설치할 것을 권장함. PostCSS를 사용할 수 없는 환경인 경우에 다른 방법으로 설치할 수는 있으나 PostCSS를 사용하면 auto prefix와 같이 편리한 기능을 사용할 수 있기 때문에 나 또한 PostCSS를 사용하기를 추천. 그러므로 이 글에서는 PostCSS 플러그인으로 설치하는 방법을 소개함.
Tailwind CSS v2 부터는 IE를 지원하지 않기 때문에 PostCSS의 autoprefixer 등을 함께 사용해야 함.
npm install tailwindcss@latest postcss@latest autoprefixer@latest
// postcss.config.js 등의 postcss 설정 파일
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
npx tailwindcss init
위의 명령어를 입력하면 프로젝트 루트에 아래의 설정 파일이 생성됨. 기본 스타일이나 플러그인 등을 설정할 수 있는 파일.
// tailwind.config.js
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {},
plugins: [],
};
메인 CSS 파일에 아래 코드를 추가하여 Tailwind CSS 스타일을 추가함. @tailwind 디렉티브는 빌드 시에 CSS 코드로 변환됨.
@tailwind base;
@tailwind components;
@tailwind utilities;
만일 React나 Vue와 같이 CSS 파일을 JavaScript에 직접 임포트할 수 있는 경우에는 아래와 같이 JavaScript 코드에서 직접 임포트할 수도 있음.
파일 크기를 최소화하기 위해 purge 옵션을 사용한다. 프로덕션으로 빌드할 때 여기에 설정된 파일에서 사용되지 않는 모든 클래스는 제거됨.
위에서 말했듯 Tailwind CSS는 기본 스타일 값을 쉽게 커스텀할 수 있음. 아래와 같이 색상이나 특정 스타일, 변수 등 웬만한 것을 커스텀할 수 있고, 원한다면 JavaScript 코드를 활용할 수도 있음.
더 자세한 내용은 여기에서 확인.
기본 스타일을 설정할 때 주의할 점은 실수로 기본 스타일 값을 오버라이딩 하지 않는 것. extend
객체 안에 넣어주지 않으면 기본 스타일 값이 덮어써지므로 아래와 같이 extend
를 잘 활용하도록 함.
위의 코드와 같이 클래스명을 입력하는 것만으로 스타일링을 할 수 있음. 위에서 사용한 각 클래스는 아래와 같은 스타일을 가지게 됨.
이 외의 모든 유틸리티 클래스는 여기에서 찾을 수 있음.
만일 버튼이나 사이드바 메뉴와 동일한 스타일을 여러 번 사용하게 되는 경우 아래처럼 유틸리티 클래스를 추가로 만들어서 사용할 수도 있음.
hover:xx
와 같은 형태로 상태 스타일을 손쉽게 설정할 수 있음.
자식 태그들을 그룹핑하여 부모 태그의 상태값에 따라 반영되도록 설정할 수도 있음.
반응형에 대한 스타일도 쉽게 설정이 가능함. 모바일 퍼스트이며 md:XX
와 같이 prefix에 설정한 breakpoint 값에 따라 변경됨.
리스트 등을 구현할 때 각 아이템 사이의 간격을 쉽게 설정할 수 있음.
메뉴 등을 스타일링할 때 마지막 메뉴만 오른쪽 마진을 주지 않는 등의 자식 요소 설정도 쉽다. 자식 태그 클래스에 hover:
와 같이 last:
프리픽스를 사용함.
단, last: 프리픽스는 기본으로 설정되어 있지 않기 때문에 아래와 같은 설정이 필요함.
Tailwind는 주로 사용하는 CSS를 모아둔 집합으로 보아도 무방합니다. fontSize를 예시로 들면, 크기를 나타내는 숫자에 무한한 값이 올 수 있는데, Tailwind에는 주로 사용되는 숫자를 모아두고 이 중에서 사용할 수 있도록 해줌. fontSize는 ‘xs’, ‘sm’, ‘base’, ‘lg’, ‘xl’, ‘2xl’, …, ‘9xl’ 의 크기만 지원하고 있어서 이 중에서 선택하여 사용할 수 있음. 이 크기만으로 디자인이 구성되면 문제가 없겠지만 이 외의 값으로 크기를 설정해야 하는 경우 Tailwind에서는 대괄호로 감싸서 arbitrary value를 입력할 수 있는 기능을 제공하고 있음. 그런데 때에 따라 이 ‘Arbitrary values’ 기능을 매우 자주 사용해야 하는 경우도 발생함. 예를 들어 rem이 아닌 px 단위로 디자인 요구사항을 맞추어야 하는 경우에는 상당히 빈번하게 사용해야 함.
와 같은 코드를 작성하게 될 수도 있음. 이는 작성할 때도 피로하고, 한 줄이 차지하는 너비가 길어지므로 코드를 읽는 개발자에게도 좋지 않음.
이러한 부분들은 테마를 커스터마이징하여 해결할 수 있음.
만약 px단위로 디자인 요구사항을 충족해야 하는 경우라면 다음과 같이 미리 px에 대한 프리셋을 전부 ‘extend’로 등록할 수 있음.
// tailwind.config.js
const px0_10 = { ...Array.from(Array(11)).map((_, i) => `${i}px`) };
const px0_100 = { ...Array.from(Array(101)).map((_, i) => `${i}px`) };
const px0_200 = { ...Array.from(Array(201)).map((_, i) => `${i}px`) };
module.exports = {
theme: {
extend: {
borderWidth: px0_10,
fontSize: px0_100,
lineHeight: px0_100,
minWidth: px0_200,
minHeight: px0_200,
spacing: px0_200,
...
}
}
}
이렇게 한 번 등록하고 나면 고민 없이 디자인 명세에 표기된 수치를 그대로 옮기기만 하면 되어 편리함.
위에 꺼냈던 예시도 다음과 같이 보다 간단한 형태로 작성할 수 있음.
코드에서 사용할 크기에 대한 단위를 rem이나 px 등으로 통일하고, Tailwind에서 제공하는 기본 범위보다 조금 더 촘촘하고 넓은 범위로 미리 ‘extend’에 등록한 결과라고 할 수 있음. ‘arbitrary value’ 중에 필요한 부분들을 모두 alias 설정하는 것으로 이해해도 됨.
단, 테마가 생각보다 세분화되어 있어 꽤 많은 프리셋을 등록해야 함. 예를 들어 ‘spacing’은 padding, margin, width, height, maxHeight, top, gap 등 크기에 대한 많은 것을 다루기는 하지만, minWidth, minHeight에 대해 alias를 등록하려면 ‘theme extend’에 spacing이 아닌 minWidth, minHeight를 별개로 등록해야 함. 크기를 지정할 때 쓰이는 fontSize, borderWidth, lineHeight 등도 마찬가지로 ‘theme extend’에 추가해주어야 하므로, 주로 사용하는 CSS가 ‘theme extend’에 등록되었는지 확인한 후 축약된 표현을 사용해야 함. 위의 tailwind.config.js 예시에 ‘extend’에 등록된 것이 꽤 많았던 것도 이러한 이유.
참고로 Tailwind가 v2에서 v3으로 업데이트되면서 “flex-[2_2_0%]”, “col-[2/-2]” 등 여러 숫자를 조합하는 CSS의 arbitrary value도 지원함. 이렇게 조합하는 케이스는 미리 ‘extend’에 등록해두려면 경우의 수가 많기 때문에 만일 아직 Tailwind 버전이 v3이 아니라면 v3으로 업데이트하여 오히려 arbitrary value를 적극적으로 사용하는 것을 추천.
사전적 의미는 비행 전의
라는 의미입니다. 디자인 시스템을 따라 Tailwind를 적용하다 보면 브라우저에서 기본적으로 설정하는 스타일과 충돌이 발생할 수 있는데, 이를 피하고자 브라우저에서 기본적으로 설정하는 스타일을 무효화 혹은 백지화하는 것. 예를 들면 heading element인 h1, h2, …, h6 등에는 다음과 같이 설정되어 있음.
이러한 preflight는 @tailwind base 에 설정이 되어 있음.
또 다른 예시로 svg의 경우는 다음과 같이 적용되어 있음.
무심코 h1 태그를 사용하면서 폰트 크기가 크게 출력될 것으로 예상하는 실수를 범할 수 있으니 주의해야 함.
Preflight를 원치 않는 경우 다음처럼 config 파일을 설정하여 비활성화시킬 수도 있음.
React를 예시로 ConfirmButton이라는 컴포넌트를 구현해본다고 가정함. 컴포넌트에 style variant를 주기 위해 다음과 같이 바탕은 흰색이고 글자는 파란색이며 rounded 처리된 스타일( 예시 )을 미리 등록해놓고 사용하는 방법이 있음.
혹은 다음과 같이 className을 컴포넌트의 props로 넘겨주며 무한한 variant로 스타일을 지정할 수도 있음.
디자인 시스템이 견고하게 짜여 있는 경우 주로 디자인의 변화가 줄어들게 되어, 바로 위와 같이 스타일의 자유도가 높은 설계는 거의 마주할 일이 없음. 하지만 Tailwind의 장점인 className으로부터 스타일을 바로 유추할 수 있다는 점을 활용하여 개발자의 편의상 컴포넌트에 직접 className을 props로 받아 스타일을 변경할 수 있도록 설계하는 경우도 간혹 발생하게 됨. 위의 두 방법 중 전자의 형태가 조금 더 바람직한 설계로 생각되지만, 부득이하게 후자의 형태로 컴포넌트를 설계한다고 가정해 봄.
결론부터 이야기하면, 이때 흔히 마주하게 되는 문제가 있고, 이의 해결책으로 twin.macro 를 활용하면 그 문제를 쉽게 해결할 수 있음. 이제 이 문제와 해결책을 살펴보도록 함.
컴포넌트의 내부 구현을 최대한 단순하게 다음과 같이 구성해 봄.
default로 설정된 스타일이 존재하고, className을 props로 전달받은 경우에 default로 설정된 스타일이 아니라 props로 전달받은 스타일을 적용하려는 의도.
하지만 의도한 대로 동작할까요?
위의 코드는 다음과 같은 결과물로 보여짐.
결과
className으로 설정한 흰색 바탕과 파란 글자가 아니라 컴포넌트 내부에서 default로 설정해둔 검은색 바탕에 붉은 글자가 출력됨. default로 설정된 className과 props로 넘겨받은 className을 병합하려고 할 때 의도대로 동작하지 않는 문제.
이 현상을 조금 더 단순한 형태로 변경하여 살펴봄.
예를 들어, 다음의 경우에 우선순위는 명확함. child에 위치한 text-blue-400 가 적용되는 것을 쉽게 예측할 수 있음.
결과: Hello world
다음의 경우는 어떠할까요?
과: Hello world
하나의 element에서 두 클래스가 color라는 같은 CSS 속성을 다루고 있음. 이 경우, 아무리 text-red-400, text-blue-400의 순서를 바꾸어도 text-red-400이 적용됨. 😭
Tailwind CSS 빌드의 결과물을 살펴보면 원인을 알 수 있음.
Tailwind에서는 수많은 클래스들을 사용할 수 있지만, 이 중에서 소스 코드에 선언된 클래스를 검출하여 스타일을 구성함. 이때 소스 코드에서 클래스가 출현한 빈도나 출현한 순서와는 상관없이 Tailwind에서 정의한 순서에 따라 output.css처럼 스타일을 구성함. 따라서 하나의 element에 위의 두 클래스가 동시에 적용되면 뒤에 선언된 .text-red-400이 cascading에 의해 .text-blue-400 보다 항상 우선하게 됨. 이러한 우선순위를 간과하는 경우가 많고, default로 설정된 className과 props로 넘겨받은 className을 병합하는 과정에서 뒤에 위치한 className이 적용될 것으로 착각하면 의도하지 않은 결과를 낳을 수 있음.
하나의 엘리먼트에 같은 속성을 정의하는 CSS가 두 개 이상 설정되어 경합이 발생할 수 있는 경우에 twin.macro 가 해결책이 될 수 있음. twin.macro는 className에 선언된 여러 클래스 중에 뒤에 위치한 클래스가 최종적으로 적용되도록 해줌.
twin.macro를 사용하여 다음과 같이 className을 선언하면 뒤에 위치한 text-blue-400이 더 높은 우선순위를 갖게 됨.
twin.macro는 혹여나 개발자가 실수로 같은 속성의 스타일을 하나의 요소에 중복으로 설정하더라도 결과를 명확히 예측할 수 있기 때문에 생산성 측면에도 도움이 됨.
참고로 아직까지 twin.macro가 Tailwind v3에서 완전히 사용할 수 있는 단계는 아님. 현재 ‘Tailwind v3 updates’라는 이슈를 통해 업데이트 중.
twin.macro의 원리를 간단하게 살펴봄.
twin.macro 동작 원리
요약하면, className에 선언된 클래스의 CSS 속성을 key로 하여 key에 값을 덮어쓰는 원리. className의 앞에 선언된 class부터 일종의 맵처럼 CSS 속성을 key로, 값을 value로 하여 Object에 덮어쓰는 과정을 반복. 이 과정에서 key가 같아서 경합이 발생하면 뒤에 선언된 스타일이 앞에 선언된 스타일을 덮어씀.
세부적인 과정은 다음과 같음.
1. babelPluginMacros 로 매크로를 등록함.
2. 노드를 순회하며 twin.macro로 선언된 스타일들을 전부 찾음.
3. string[] 으로 선언된 스타일을 Object로 변환함.
4. className을 순회하며 Object에 스타일을 계속 덮어씀.
하나의 element에 많은 CSS를 설정해야 하는 경우가 있음.
<div className="sticky top-0 bg-white z-10 flex sm:grid items-center sm:items-end grid-cols-2 sm:grid-cols-3 p-10 border-b pt-15 border-gray top-0">
이런 경우에 한눈에 모든 내용이 다 들어오지 않기 때문에 자칫하다가는 앞에 있는 CSS를 인지하지 못하고 뒤에 똑같은 CSS를 또 기입하는 휴먼 에러가 발생할 수 있습니다. (위의 예시에는 top-0 이 중복으로 들어가 있습니다)
이런 부분을 조금 해소할 수 있는 플러그인이 있는데, Tailwind의 클래스를 정렬해주는 기능을 하는 Headwind 입니다. 알파벳 순의 정렬은 아니고 스타일의 카테고리대로 정렬해주어 코드를 이해하는데에도 조금 더 수월해집니다.
참고
https://wonny.space/writing/dev/hello-tailwind-css
yled-jsx로-스타일링-하기
https://doing-nothing.tistory.com/112
https://fe-developers.kakaoent.com/2022/220303-tailwind-tips/
https://fe-developers.kakaoent.com/2022/221013-tailwind-and-design-system/
https://tailwindcss.com/
https://hkreal.tistory.com/3