01 — Media Queries
답하는 질문:
@media는 무엇을 본다고 했을 때, 그 무엇은 정확히 어디까지인가?
한 줄 답
@media는 뷰포트의 차원(width/height/orientation/aspect-ratio)뿐 아니라 디바이스의 능력(hover/pointer/color-gamut/resolution)과 사용자의 선호(prefers-color-scheme/reduced-motion/contrast)까지 본다. 즉 반응 대상 = 환경 전체다. 2022년 Range Syntax(400px <= width <= 800px) 추가로 표현력이 한 번 더 늘었다.
Why — 왜 미디어 쿼리인가
1) 출발: print 스타일시트
CSS1(1996)에는 @media print만 있었다. “화면용 스타일 / 인쇄용 스타일을 분리한다” 가 최초 목표. 이는 미디어 타입이다 — 출력 매체 자체.
2) 2009 CSS3 Media Queries — 뷰포트 차원의 등장
@media (min-width: 768px) 같은 미디어 피쳐 도입. 이때부터 타입(=무엇으로) 과 피쳐(=얼마만큼) 가 분리됐다.
3) 2017+ — 사용자 환경 피쳐
prefers-color-scheme(Safari 12.1, 2019) — OS의 다크 모드 감지.prefers-reduced-motion(Safari 10.1, 2017) — 접근성 동작 축소.prefers-contrast,forced-colors— Windows 고대비 모드.hover,pointer(Media Queries Level 4) — 입력 디바이스의 능력.
이로써 미디어 쿼리는 디바이스 크기 측정 도구에서 환경 적응 언어로 확장됐다.
4) 2022 Range Syntax
@media (width >= 400px) and (width <= 800px) 같은 수학적 표현. 이전의 min-width: 400px and max-width: 800px보다 경계 포함성이 명확하다.
How — 어떻게 평가되는가
- 평가 시점: 매 layout pass 마다 — 리사이즈, 회전, OS 설정 변경 시 자동 재평가.
- JavaScript에서 구독:
window.matchMedia('(min-width: 768px)').addEventListener('change', ...). - CSS Variables와 조합 가능: 변수 자체에
var(--bp, 768px)를 쓸 수는 없지만(custom property는 미디어 쿼리에서 평가 불가), 미디어 쿼리 블록 안에서 변수를 재할당하는 패턴은 표준이다.
What — 표현 가능한 모든 것
1) 미디어 타입
@media screen { /* 화면 */ }
@media print { /* 인쇄 미리보기·인쇄 */ }
@media all { /* 모든 매체(기본) */ }
/* speech, tty, projection, handheld 등은 deprecated */2) 뷰포트 차원
/* 전통 문법 */
@media (min-width: 768px) { ... }
@media (max-width: 1199px) { ... }
@media (min-width: 768px) and (max-width: 1199px) { ... }
/* Range Syntax (2022+) */
@media (width >= 768px) { ... }
@media (768px <= width <= 1199px) { ... }
@media (height < 600px) { ... }
@media (aspect-ratio >= 16/9) { ... }
@media (orientation: landscape) { ... }3) 디바이스 능력 (Level 4)
| 피쳐 | 값 | 의미 |
|---|---|---|
hover | hover / none | 마우스처럼 hover 가능한가 |
pointer | fine / coarse / none | 정밀도 — 마우스(fine) vs 터치(coarse) |
any-hover | hover / none | 입력 중 하나라도 hover 가능 |
any-pointer | fine / coarse | 입력 중 하나라도 정밀 |
color-gamut | srgb / p3 / rec2020 | 표현 가능한 색공간 |
resolution | 2dppx 등 | 디스플레이 픽셀 밀도 |
dynamic-range | standard / high | HDR 지원 |
/* 터치 디바이스에서만 큰 탭 타겟 */
@media (pointer: coarse) {
button { min-height: 44px; }
}
/* P3 색공간이면 더 진한 색 사용 */
@media (color-gamut: p3) {
:root { --accent: oklch(70% 0.25 250); }
}4) 사용자 선호 (Level 5)
@media (prefers-color-scheme: dark) { :root { --bg: #111; } }
@media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; } }
@media (prefers-contrast: more) { :root { --text: #000; --bg: #fff; } }
@media (prefers-reduced-transparency: reduce) { .glass { backdrop-filter: none; } }
@media (forced-colors: active) { /* Windows 고대비 모드 */ }5) 논리 결합
@media (min-width: 768px) and (orientation: landscape) { ... }
@media (max-width: 767px), (orientation: portrait) { ... } /* OR */
@media not all and (min-width: 768px) { ... } /* NOT */
@media (hover: hover) and (pointer: fine) { ... } /* 데스크탑 정밀 입력 */6) 흔히 쓰는 브레이크포인트 (참조)
| 이름 | 폭 | 출처 |
|---|---|---|
| sm | 640px | Tailwind |
| md | 768px | Tailwind / iPad portrait |
| lg | 1024px | Tailwind / iPad landscape |
| xl | 1280px | Tailwind |
| 2xl | 1536px | Tailwind |
주의: 디바이스 크기를 맞추려 하지 말고, 콘텐츠가 깨지는 지점을 브레이크포인트로 잡는다 — Marcotte 본인의 후일담.
What-if — 잘못 쓰면
Case 1: min-width와 max-width의 경계 중첩
/* 768px 정확히는 어디에 속하나? */
@media (max-width: 768px) { body { background: red; } }
@media (min-width: 768px) { body { background: blue; } }
/* 둘 다 매칭됨 — 캐스케이드 순서대로 blue가 이김 */해결 — Range Syntax로 명시:
@media (width < 768px) { body { background: red; } }
@media (width >= 768px) { body { background: blue; } }Case 2: prefers-reduced-motion 무시
애니메이션을 켜둔 채 출시 → 전정장애 사용자에게 멀미. WCAG 2.3.3 위반.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}Case 3: hover 가정으로 모바일에서 메뉴 안 열림
/* 안티패턴 */
.menu:hover .dropdown { display: block; }터치에는 hover가 없다 — 클릭 한 번이면 즉시 close. :focus-within과 묶거나 (hover: hover) 가드:
@media (hover: hover) {
.menu:hover .dropdown { display: block; }
}Case 4: 미디어 쿼리에서 CSS 변수 사용
:root { --bp-md: 768px; }
@media (min-width: var(--bp-md)) { ... } /* X — 평가 안 됨 */미디어 쿼리는 parse time에 평가되므로 custom property를 알 수 없다. PostCSS 변수, Sass, 또는 환경 변수(env(--bp-md), 제안 단계)로만 가능.
Insight — Range Syntax는 왜 14년이 걸렸나
min-width와 max-width의 경계 모호성은 2010년부터 알려진 문제였다. 그런데 표준화는 2022년에야 끝났다.
이유는 CSS의 문법적 제약이다. CSS의 토큰 파서는 < > 같은 비교 연산자를 가진 적이 없었다 — 처음에는 SGML 호환을 위해 < 를 예약어로 피했다. 이를 풀려면 전체 파서를 손봐야 했다.
2018년 Tab Atkins(CSSWG 의장)가 “이게 단순한 문법 추가가 아니라 CSS 토큰 모델을 바꾸는 일” 이라고 PR에서 밝힌 적이 있다. 그래서 늦었다.
Container Queries도 같은 이유로 늦었다 — 순환 의존성(자식이 부모 크기에 반응하면, 자식이 부모 크기를 바꿔서 무한 루프). CSS Containment(2019)가 layout containment를 격리한 후에야 가능해졌다.
즉 CSS의 진화 속도는 문법이 아니라 순환 의존성 관리에 의해 결정된다.
요약 + Mermaid
@media는 타입 + 차원 + 능력 + 선호 4축을 본다.- Range Syntax(
<=>=)로 경계 모호성 해결 — 2022 표준. prefers-*는 접근성 필수 — 무시는 WCAG 위반.(hover: hover)가드 없으면 터치에서 깨진다.- 미디어 쿼리에서 CSS 변수는 평가 불가 — PostCSS/Sass 우회.
다음: 02-container-queries — 뷰포트가 아니라 부모에 반응하는 법.