반응형 디자인과 접근성
Tailwind CSS v4의 반응형 기법, 컨테이너 쿼리, 접근성, 성능 최적화
코드 블록의 Try it Yourself 버튼으로 직접 실행할 수 있다.
구문
반응형 디자인과 접근성
1. Tailwind의 반응형 시스템
Tailwind는 모바일 우선(Mobile First) 방식을 사용한다. 접두사가 없는 클래스는 모든 화면에 적용되고, 접두사가 붙은 클래스는 해당 브레이크포인트 이상에서 적용된다.
1.1. 기본 브레이크포인트
| 접두사 | 최소 너비 | 대상 기기 |
|---|---|---|
| (없음) | 0px | 모든 화면 (모바일 기본) |
sm: | 40rem (640px) | 큰 모바일·소형 태블릿 |
md: | 48rem (768px) | 태블릿 |
lg: | 64rem (1024px) | 노트북·데스크톱 |
xl: | 80rem (1280px) | 와이드 데스크톱 |
2xl: | 96rem (1536px) | 초대형 화면 |
v4 변경사항 — v3에서
sm: 640px등 px 단위였던 것이 v4에서 rem 단위로 변경됐다. 브라우저 폰트 크기 설정을 존중하는 방식으로 개선됐다.
1.2. 모바일 우선 작성 원칙
<div class="text-sm md:text-base lg:text-lg">
모바일: 14px → 태블릿: 16px → 데스크톱: 18px
</div>
<div class="flex flex-col md:flex-row gap-6">
<aside class="w-full md:w-64 flex-shrink-0">사이드바</aside>
<main class="flex-1">메인 콘텐츠</main>
</div>
2. 반응형 유틸리티 패턴
2.1. 반응형 그리드
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div class="bg-white rounded-2xl shadow p-4">카드 1</div>
<div class="bg-white rounded-2xl shadow p-4">카드 2</div>
<div class="bg-white rounded-2xl shadow p-4">카드 3</div>
<div class="bg-white rounded-2xl shadow p-4">카드 4</div>
</div>
2.2. 반응형 요소 표시/숨김
<div class="hidden lg:block">데스크톱 전용 콘텐츠</div>
<div class="block lg:hidden">모바일 전용 콘텐츠 (햄버거 메뉴 등)</div>
<div class="hidden sm:block lg:hidden">태블릿 전용</div>
2.3. 반응형 타이포그래피
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900">
반응형 제목
</h1>
<p class="text-sm sm:text-base md:text-lg leading-relaxed text-gray-600">
화면 크기에 따라 본문 텍스트도 조정됩니다.
</p>
2.4. 반응형 간격
<section class="px-4 sm:px-6 md:px-8 lg:px-12 py-8 sm:py-12 lg:py-16">
<div class="max-w-6xl mx-auto">
<h2 class="text-2xl lg:text-3xl font-bold mb-4 sm:mb-6 lg:mb-8">
섹션 제목
</h2>
</div>
</section>
2.5. 반응형 이미지
<div class="aspect-video sm:aspect-[4/3] lg:aspect-video overflow-hidden rounded-2xl">
<img src="/hero.jpg" alt="히어로 이미지"
class="w-full h-full object-cover" />
</div>
<div class="relative h-64 sm:h-80 md:h-96 lg:h-[500px] overflow-hidden">
<img src="/hero-bg.jpg" alt=""
class="absolute inset-0 w-full h-full object-cover" />
<div class="absolute inset-0 bg-black/40 flex items-center justify-center">
<h1 class="text-3xl md:text-5xl font-bold text-white text-center px-4">
히어로 타이틀
</h1>
</div>
</div>
3. v4 커스텀 브레이크포인트
v3에서는 tailwind.config.js의 theme.screens에서 설정했다.
v4에서는 CSS의 @theme에서 정의한다.
/* src/index.css */
@import "tailwindcss";
@theme {
/* 기존 브레이크포인트 덮어쓰기 */
--breakpoint-sm: 36rem; /* 576px */
--breakpoint-md: 48rem; /* 768px */
--breakpoint-lg: 62rem; /* 992px */
--breakpoint-xl: 75rem; /* 1200px */
/* 커스텀 브레이크포인트 추가 */
--breakpoint-xs: 22.5rem; /* 360px — 소형 모바일 */
--breakpoint-3xl: 112rem; /* 1792px — 초대형 */
}
<div class="text-xs xs:text-sm sm:text-base">작은 화면도 대응</div>
<div class="max-w-screen-xl 3xl:max-w-screen-3xl mx-auto">초대형 레이아웃</div>
4. v4 컨테이너 쿼리 (Container Queries)
v4 주요 신기능 — 뷰포트가 아닌 부모 컨테이너 크기에 따라 스타일을 적용할 수 있다. v3에서는
@tailwindcss/container-queries플러그인이 필요했지만 v4에서는 기본 내장됐다.
4.1. 기본 사용법
<div class="@container">
<div class="flex flex-col @md:flex-row gap-4">
<img src="/product.jpg" alt="" class="w-full @md:w-48 rounded-xl object-cover" />
<div>
<h3 class="text-lg @md:text-xl font-bold">상품명</h3>
<p class="text-sm text-gray-500">설명...</p>
</div>
</div>
</div>
4.2. 컨테이너 쿼리 브레이크포인트
| 접두사 | 컨테이너 최소 너비 |
|---|---|
@xs: | 20rem (320px) |
@sm: | 24rem (384px) |
@md: | 28rem (448px) |
@lg: | 32rem (512px) |
@xl: | 36rem (576px) |
@2xl: | 42rem (672px) |
@3xl: | 48rem (768px) |
4.3. 컨테이너 쿼리 실전 예시
<div class="@container" id="card-container">
<div class="grid grid-cols-1 @sm:grid-cols-2 @lg:grid-cols-3 gap-4">
<div class="bg-white rounded-xl shadow p-4">카드 A</div>
<div class="bg-white rounded-xl shadow p-4">카드 B</div>
<div class="bg-white rounded-xl shadow p-4">카드 C</div>
</div>
</div>
<div class="@container/main">
<div class="@container/sidebar">
<p class="text-sm @sm/main:text-base @md/main:text-lg">
main 컨테이너 크기 기준
</p>
</div>
</div>
5. 접근성(Accessibility) 모범 사례
5.1. 색상 대비
WCAG 기준: 일반 텍스트 4.5:1 이상, 큰 텍스트(18px+) 3:1 이상.
<p class="text-gray-900 bg-white">충분한 대비 (21:1)</p>
<p class="text-gray-700 bg-white">충분한 대비 (약 10:1)</p>
<p class="text-white bg-blue-700">충분한 대비 (약 5.9:1)</p>
5.2. 포커스 표시 (focus-visible)
키보드 사용자를 위해 포커스 상태는 반드시 시각적으로 표시해야 한다.
focus-visible:은 키보드 탐색 시에만 링을 표시한다(마우스 클릭 시 미표시).
<button class="focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500
focus-visible:ring-offset-2 bg-blue-600 text-white px-4 py-2 rounded-lg">
접근성 버튼
</button>
<a href="#"
class="text-blue-600 underline focus:outline-none
focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:rounded">
접근성 링크
</a>
5.3. 스크린 리더 전용 텍스트
시각적으로는 숨기되 스크린 리더에는 읽히는 텍스트.
<button class="bg-gray-200 p-2 rounded-lg hover:bg-gray-300">
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span class="sr-only">닫기</span>
</button>
<button aria-label="즐겨찾기 추가" class="p-2 rounded-full hover:bg-gray-100">
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
</svg>
</button>
5.4. 의미론적 HTML + ARIA
<nav aria-label="주 메뉴">
<ul class="flex gap-4">
<li><a href="/" class="font-medium" aria-current="page">홈</a></li>
<li><a href="/about" class="text-gray-600 hover:text-gray-900">소개</a></li>
</ul>
</nav>
<div role="status" aria-live="polite" class="sr-only" id="live-region">
</div>
<div class="relative">
<button
aria-haspopup="true"
aria-expanded="false"
id="menu-button"
class="px-4 py-2 bg-white border rounded-lg"
>
메뉴 열기
</button>
<ul
role="menu"
aria-labelledby="menu-button"
class="absolute mt-2 bg-white shadow-lg rounded-xl py-1"
>
<li role="menuitem"><a href="#" class="block px-4 py-2 hover:bg-gray-50">항목 1</a></li>
<li role="menuitem"><a href="#" class="block px-4 py-2 hover:bg-gray-50">항목 2</a></li>
</ul>
</div>
<div role="status" class="inline-flex items-center gap-2">
<svg class="animate-spin size-5 text-blue-600" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
<span class="sr-only">로딩 중...</span>
</div>
<div role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
aria-label="파일 업로드 진행도 60%"
class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300 w-[60%]"></div>
</div>
5.5. 폼 접근성
<form class="space-y-4">
<div>
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">
검색어
<span class="text-red-500 ml-0.5" aria-hidden="true">*</span>
<span class="sr-only">(필수)</span>
</label>
<input
type="search"
id="search"
name="search"
required
aria-required="true"
aria-describedby="search-hint search-error"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm
focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p id="search-hint" class="mt-1 text-xs text-gray-500">
2글자 이상 입력하세요.
</p>
<p id="search-error" role="alert" class="mt-1 text-xs text-red-600 hidden">
필수 입력 항목입니다.
</p>
</div>
<fieldset class="border border-gray-200 rounded-xl p-4">
<legend class="text-sm font-medium text-gray-700 px-1">배송 방법</legend>
<div class="space-y-2 mt-2">
<label class="flex items-center gap-2">
<input type="radio" name="shipping" value="standard"
class="text-blue-600 focus:ring-blue-500" />
<span class="text-sm text-gray-700">일반 배송</span>
</label>
<label class="flex items-center gap-2">
<input type="radio" name="shipping" value="express"
class="text-blue-600 focus:ring-blue-500" />
<span class="text-sm text-gray-700">빠른 배송</span>
</label>
</div>
</fieldset>
</form>
5.6. 키보드 접근성 — 트랩 포커스 (모달)
// 모달 내 포커스 트랩
function trapFocus(modalElement) {
const focusable = modalElement.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusable[0]
const last = focusable[focusable.length - 1]
modalElement.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return
if (e.shiftKey) {
if (document.activeElement === first) { e.preventDefault(); last.focus() }
} else {
if (document.activeElement === last) { e.preventDefault(); first.focus() }
}
})
}
<a href="#main-content"
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4
focus:z-50 focus:bg-blue-600 focus:text-white
focus:px-4 focus:py-2 focus:rounded-lg focus:font-medium">
본문으로 건너뛰기
</a>
<nav>...</nav>
<main id="main-content" tabindex="-1">...</main>
6. v4 not-* variant (신규)
특정 상태가 아닐 때 스타일을 적용한다.
<ul>
<li class="not-last:border-b border-gray-200 py-3 px-4">항목 1</li>
<li class="not-last:border-b border-gray-200 py-3 px-4">항목 2</li>
<li class="not-last:border-b border-gray-200 py-3 px-4">항목 3</li>
</ul>
<button class="not-disabled:hover:bg-blue-700 bg-blue-600 text-white px-4 py-2 rounded-lg
disabled:opacity-50 disabled:cursor-not-allowed">
버튼
</button>
7. 성능 최적화
7.1. v4에서 자동 처리되는 것들
v3에서 수동으로 설정해야 했던 항목들이 v4에서 자동화됐다.
| v3 수동 작업 | v4 자동 처리 |
|---|---|
purge 옵션 설정 | 사용한 클래스만 자동 포함 |
mode: 'jit' 설정 | 항상 JIT 모드 |
@tailwind base/components/utilities | @import "tailwindcss" 한 줄 |
7.2. CSS 크기 최소화 전략
/* src/index.css */
@import "tailwindcss";
@theme {
/* 필요한 색상만 정의 — 미정의 색상은 생성되지 않음 */
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
/* 기본 gray 계열 외에 불필요한 색상 팔레트 제거 */
/* v4에서는 사용하지 않은 유틸리티는 자동으로 번들에서 제외됨 */
}
7.3. 레이어 분리
@import "tailwindcss";
/* base: HTML 요소 기본 스타일 초기화·설정 */
@layer base {
*, *::before, *::after { box-sizing: border-box; }
html { font-family: theme(--font-sans); }
h1 { @apply text-3xl font-bold text-gray-900; }
h2 { @apply text-2xl font-semibold text-gray-900; }
a { @apply text-blue-600 hover:text-blue-700; }
}
/* components: 재사용 컴포넌트 클래스 */
@layer components {
.container-main {
@apply max-w-6xl mx-auto px-4 sm:px-6 lg:px-8;
}
}
/* utilities: 커스텀 유틸리티 */
@layer utilities {
.text-balance { text-wrap: balance; }
}
7.4. 중요(Critical) CSS 인라인 처리
초기 렌더링을 빠르게 하려면 화면 상단(above-the-fold) 스타일을 인라인으로 삽입한다.
<!DOCTYPE html>
<html>
<head>
<style>
/* 히어로 섹션, 네비게이션 등 최초 표시 요소 스타일만 */
.nav { display: flex; align-items: center; padding: 1rem; }
.hero { min-height: 100svh; background: #1e3a8a; }
</style>
<link rel="preload" href="/dist/style.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/dist/style.css"></noscript>
</head>
7.5. Vite 프로덕션 빌드
# 개발 서버
npm run dev
# 프로덕션 빌드 (자동 최소화·트리셰이킹)
npm run build
# 빌드 결과물 미리보기
npm run preview
// vite.config.js — CSS 최소화 설정
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
build: {
cssMinify: true, // CSS 압축
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'], // 벤더 청크 분리
}
}
}
}
})
8. 반응형 + 접근성 통합 실전 예시
8.1. 접근성을 갖춘 반응형 카드 목록
<section aria-labelledby="products-heading">
<h2 id="products-heading" class="text-2xl font-bold text-gray-900 mb-6">
상품 목록
</h2>
<p role="status" class="sr-only">총 6개의 상품이 있습니다.</p>
<ul class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"
role="list">
<li>
<article class="bg-white rounded-2xl shadow hover:shadow-lg transition-shadow
focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2">
<img src="/product1.jpg" alt="스니커즈 A - 흰색 운동화" class="w-full h-48 object-cover rounded-t-2xl" />
<div class="p-4">
<h3 class="font-bold text-gray-900">스니커즈 A</h3>
<p class="text-sm text-gray-500 mt-1">편안하고 가벼운 일상 스니커즈</p>
<div class="flex items-center justify-between mt-3">
<span class="text-lg font-bold">₩89,000</span>
<a href="/products/1"
class="text-sm font-medium text-blue-600 hover:text-blue-800
focus:outline-none rounded
after:absolute after:inset-0"
aria-label="스니커즈 A 상세 보기">
상세 보기
</a>
</div>
</div>
</article>
</li>
</ul>
</section>
8.2. 반응형 테이블
<div class="overflow-x-auto rounded-2xl border border-gray-200">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
이름
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider hidden sm:table-cell">
이메일
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
상태
</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">
작업
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<div class="size-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-xs font-bold flex-shrink-0">
홍
</div>
<span class="text-sm font-medium text-gray-900">홍길동</span>
</div>
</td>
<td class="px-4 py-3 hidden sm:table-cell">
<span class="text-sm text-gray-600">hong@example.com</span>
</td>
<td class="px-4 py-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
활성
</span>
</td>
<td class="px-4 py-3 text-right">
<button class="text-sm text-blue-600 hover:text-blue-800 font-medium
focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 rounded">
편집
</button>
</td>
</tr>
</tbody>
</table>
</div>
9. 접근성 검사 체크리스트
| 항목 | 확인 방법 | Tailwind 관련 클래스 |
|---|---|---|
| 색상 대비 | WebAIM Contrast Checker | text-gray-900, bg-white 등 명도 조합 확인 |
| 포커스 표시 | Tab키로 탐색 | focus-visible:ring-2 focus-visible:ring-{color} |
| 이미지 대체 텍스트 | 개발자 도구 확인 | alt 속성 작성, 장식 이미지는 alt="" |
| 키보드 탐색 | Tab / Shift+Tab / Enter / Space | 모든 인터랙티브 요소 도달 가능 여부 |
| 스크린 리더 | NVDA(Windows), VoiceOver(Mac) | sr-only, aria-label, role 속성 |
| 의미론적 HTML | 개발자 도구 Accessibility Tree | <nav>, <main>, <section>, <h1>~<h6> 올바른 사용 |
| 반응형 | 기기별 실제 테스트 | sm:, md:, lg: 브레이크포인트 |
| 링크 맥락 | 스크린 리더로 링크 목록 탐색 | "자세히 보기" 대신 "상품명 자세히 보기" |
핵심 정리
- 모바일 우선: 접두사 없음 = 모바일,
sm:md:lg:로 확장- v4 브레이크포인트: rem 단위로 변경 (
sm: 40rem= 640px)- 컨테이너 쿼리:
@container+@sm:@md:로 부모 크기 기반 스타일 (v4 기본 내장)- 커스텀 브레이크포인트:
tailwind.config.js→ CSS@theme--breakpoint-*- 접근성:
focus-visible:ring-2,sr-only,aria-*, 의미론적 HTMLnot-*variant (v4 신규): 특정 상태가 아닐 때 적용- 성능: v4에서 purge·JIT 자동화,
@layer base/components/utilities구조 활용