
이번 글에서는 이미 알고 있었지만 다시 한 번 짚어볼 만한 내용들, 그리고 실제로 유용하게 활용한 설정들을 정리해보려 합니다.
1편을 먼저 보고 오시면 더 도움이 될 것 같습니다.
디자이너분들이 font-size를 px로 정의해주시는 경우가 많고, 저도 그동안 px로 개발해왔기 때문에 크게 신경 쓰지 않았던 부분인데요. 이번 과제를 통해 웹 접근성 측면에서 px로 폰트 크기를 고정하는 것이 좋지 않다는 점을 새롭게 알게 되었습니다.
당장 반응형을 고려하지 않더라도, 습관적으로 rem을 사용하는 것이 좋겠다고 생각했습니다.
참고로 px를 rem으로 변환할 때는 16으로 나눈 값을 사용하면 됩니다.
/* Font Size */
--font-size-22: 1.375rem; /* 22px */
--font-size-20: 1.25rem; /* 20px */
--font-size-16: 1rem; /* 16px */
--font-size-14: 0.875rem; /* 14px */
--font-size-12: 0.75rem; /* 12px */
--font-size-11: 0.6875rem; /* 11px */
사실 이번 과제에서 피드백을 받은 부분입니다. 다만 이를 수정하려면 CSS뿐만 아니라
font-*로 적용된 스타일 ex)font-heading-1을 사용하는 모든 부분을text-*기준으로 전면 수정해야 했습니다. 작업 범위가 커 공수 대비 우선순위를 고려해 이번에는 반영하지 못했습니다.. 🥲
Tailwind v4에서는 --text-* 네임스페이스를 활용하면 font-size, line-height, letter-spacing, font-weight를 하나의 유틸리티 클래스로 묶을 수 있습니다.
더블 대시(--) 접미사로 부속 속성을 연결하는 방식입니다.
--text-{이름} /* font-size */
--text-{이름}--line-height /* line-height */
--text-{이름}--letter-spacing /* letter-spacing */
--text-{이름}--font-weight /* font-weight */
공식 문서의 font-size 커스터마이징 섹션에서 확인할 수 있습니다.
이번 리디자인 프로젝트에서 사용한 text style을 예시로 해 @theme 변환을 해봅시다.
| Style | Size | Line Height | Weight | Letter Spacing |
|---|---|---|---|---|
| Heading 1 | 22px | 140% | 700 | -0.03em |
| Heading 2 | 20px | 140% | 700 | -0.03em |
| Body 1 | 16px | 150% | 700 | -0.02em |
| Body 2 | 16px | 150% | 600 | -0.02em |
| Body 3 | 16px | 150% | 400 | -0.02em |
| Body 4 | 14px | 150% | 700 | -0.02em |
| Body 5 | 14px | 150% | 600 | -0.02em |
| Body 6 | 14px | 150% | 400 | -0.02em |
| Caption 1 | 12px | 150% | 600 | -0.02em |
| Caption 2 | 12px | 150% | 400 | -0.02em |
| Caption 3 | 11px | 150% | 600 | -0.02em |
@theme {
/* Heading */
--text-heading-1: 1.375rem; /* 22px */
--text-heading-1--line-height: 1.925rem; /* 22px × 1.4 */
--text-heading-1--letter-spacing: -0.03em;
--text-heading-1--font-weight: 700;
--text-heading-2: 1.25rem; /* 20px */
--text-heading-2--line-height: 1.75rem; /* 20px × 1.4 */
--text-heading-2--letter-spacing: -0.03em;
--text-heading-2--font-weight: 700;
/* Body */
--text-body-1: 1rem; /* 16px */
--text-body-1--line-height: 1.5rem; /* 16px × 1.5 */
--text-body-1--letter-spacing: -0.02em;
--text-body-1--font-weight: 700;
--text-body-2: 1rem; /* 16px */
--text-body-2--line-height: 1.5rem;
--text-body-2--letter-spacing: -0.02em;
--text-body-2--font-weight: 600;
--text-body-3: 1rem; /* 16px */
--text-body-3--line-height: 1.5rem;
--text-body-3--letter-spacing: -0.02em;
--text-body-4: 0.875rem; /* 14px */
--text-body-4--line-height: 1.3125rem; /* 14px × 1.5 */
--text-body-4--letter-spacing: -0.02em;
--text-body-4--font-weight: 700;
--text-body-5: 0.875rem; /* 14px */
--text-body-5--line-height: 1.3125rem;
--text-body-5--letter-spacing: -0.02em;
--text-body-5--font-weight: 600;
--text-body-6: 0.875rem; /* 14px */
--text-body-6--line-height: 1.3125rem;
--text-body-6--letter-spacing: -0.02em;
/* Caption */
--text-caption-1: 0.75rem; /* 12px */
--text-caption-1--line-height: 1.125rem; /* 12px × 1.5 */
--text-caption-1--letter-spacing: -0.02em;
--text-caption-1--font-weight: 600;
--text-caption-2: 0.75rem; /* 12px */
--text-caption-2--line-height: 1.125rem;
--text-caption-2--letter-spacing: -0.02em;
--text-caption-3: 0.6875rem; /* 11px */
--text-caption-3--line-height: 1.03125rem; /* 11px × 1.5 */
--text-caption-3--letter-spacing: -0.02em;
--text-caption-3--font-weight: 600;
}
--text-body-3처럼 --font-weight를 지정하지 않으면 Tailwind는 해당 속성에 개입하지 않고 브라우저 기본값(400)을 그대로 따릅니다.
디자인 스펙에 명시된 속성만 골라 선언하면 되므로, 불필요한 오버라이드 없이 깔끔하게 스케일을 유지할 수 있습니다.
Tailwind에서 text-lg와 text-black은 둘 다 text- 네임스페이스를 공유하지만, 하나는 font-size, 다른 하나는 color에 해당합니다.
Tailwind CSS 커스텀 스케일 이름이 기존 색상 토큰 이름과 겹치면 유틸리티가 의도대로 생성되지 않을 수 있습니다. 예를 들어 --text-primary처럼 색상 토큰으로 쓸 법한 이름은 피하는 게 좋습니다.
실제로 --text-banner와 --text-color-banner를 함께 정의했을 때 font-size 유틸리티가 생성되지 않고 color 유틸리티만 생성되는 충돌이 보고된 바 있습니다.
참고 자료
Tailwind로 조건부 스타일링을 하다 보면 아래 같은 상황이 자주 생깁니다.
// 문자열 보간으로 쓰면 지저분하고, 클래스 충돌도 발생할 수 있음
className={`text-sm ${isActive ? "text-blue-500" : "text-gray-500"}`}
이를 깔끔하게 해결해주는 조합이 바로 clsx + tailwind-merge입니다.
px-2와 px-4가 동시에 적용되면 후자만 살려줍니다.cn("px-2 py-1", "px-4")
// clsx만 → "px-2 py-1 px-4" (충돌 발생)
// tailwind-merge까지 → "py-1 px-4" (px-2 제거)두 라이브러리를 합쳐 아래처럼 cn 유틸 함수로 만들어두면, 프로젝트 전반에서 편리하게 사용할 수 있습니다.
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
실제 사용 예시입니다. 현재 탭이 활성화 상태인지에 따라 배경색과 글자색을 조건부로 적용했습니다.
<button
key={path}
onClick={() => navigate(path)}
className={cn(
"rounded-100 flex flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 px-4 py-1 transition-colors duration-400 ease-in-out",
isActive ? "bg-primary-100 text-primary-400" : "text-gray-500",
)}
>
기본 클래스는 항상 적용하되, isActive 여부에 따라 색상 클래스만 교체되는 패턴입니다.
cn 덕분에 충돌 걱정 없이 조건부 스타일을 간결하게 작성할 수 있습니다.
아이콘 파일은 vite-plugin-svgr을 통해 React 컴포넌트처럼 사용했습니다. 프로젝트마다 적용해서 쓰고 있는데, 번들 최적화와 접근성 측면에서도 유리하고 사용하기도 편리하니 적극 추천드립니다.
(잊어버릴 수 있으니 초기세팅 때 함께 설정해두시는 걸 추천드립니다.)
먼저 vite-plugin-svgr을 설치합니다.
npm install vite-plugin-svgr
그리고 vite.config.js에 아래와 같이 설정을 추가합니다.
import svgr from 'vite-plugin-svgr';
export default {
plugins: [
svgr({
svgrOptions: {
dimensions: false,
},
}),
],
};
여기서 dimensions: false 옵션이 핵심입니다.
SVG 파일에는 보통 width="16" height="16" 같은 고정 크기값이 포함되어 있는데, 이 값이 남아 있으면 CSS에서 크기를 변경해도 적용되지 않습니다.
dimensions: false를 설정하면 SVGR이 빌드 시 이 고정값을 자동으로 제거해주기 때문에, Tailwind의 size-5 같은 유틸리티 클래스가 정상적으로 동작하게 됩니다.
컴포넌트로 사용할 때는 import 경로 끝에 ?react를 붙이면 됩니다.
import PlusIcon from "@/assets/icons/icon_plus_regular.svg?react";
같은 모양의 아이콘을 빨간색, 파란색 등 여러 색으로 사용해야 한다면 어떻게 할까요?
색상별로 SVG 파일을 따로 만들 필요 없이, SVG 원본의 fill이나 stroke 값을 currentColor로 설정해두면 CSS에서 color 속성만으로 색상을 자유롭게 제어할 수 있습니다.
<CheckIcon className={cn("size-4", isRead ? "text-sub" : "text-gray-400")} />
위처럼 하나의 SVG 파일만으로, 읽음 여부에 따라 초록색 또는 회색으로 렌더링할 수 있습니다.
아이콘 파일명을 icon_{이름}_{변형}.svg 형태로 통일하고 리팩토링했습니다.
파일이 많아질수록 _thin, _filled 등을 제각각 붙이다 보면 일관성이 무너지는 경험을 반복했는데, 이번 기회에 정리했습니다.
icon_{이름}_{변형}.svg 형태로 컨벤션을 잡으면 다음과 같은 이점이 있습니다.
참고 자료
사소해 보이지만 한 번 세팅해두면 두고두고 편리한 것 같습니다.
다음 프로젝트 초기 세팅 때 하나씩 챙겨보시는 걸 추천드립니다. 모쪼록 도움이 되셨으면 좋겠습니다!