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