Contents
see List모던 CSS의 패러다임 변화
최근 몇 년간 CSS는 JavaScript 없이도 복잡한 인터랙션과 레이아웃을 구현할 수 있는 방향으로 급격히 발전했다. CSS @layer로 스타일 우선순위를 명확히 관리하고, :has() 선택자로 부모 선택이 가능해졌으며, View Transitions API로 페이지 전환 애니메이션을 네이티브하게 구현할 수 있게 되었다. 이 글에서는 세 가지 핵심 기능을 실전 예제와 함께 심층적으로 다룬다.
CSS @layer — 캐스케이드 레이어 관리
CSS @layer는 스타일시트를 계층(layer)으로 분리하여 캐스케이드 우선순위를 명확하게 제어하는 기능이다. 라이브러리 스타일과 커스텀 스타일, 유틸리티 클래스 간의 충돌을 !important 없이 해결할 수 있다.
브라우저 지원: Chrome 99+, Firefox 97+, Safari 15.4+ (2022년부터 모든 주요 브라우저 지원)
/* 레이어 정의 순서 (앞이 낮은 우선순위) */
@layer reset, base, components, utilities;
/* reset 레이어 */
@layer reset {
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
}
/* base 레이어 */
@layer base {
h1 { font-size: 2rem; }
p { line-height: 1.6; }
}
/* components 레이어 */
@layer components {
.button {
padding: 0.5rem 1rem;
border-radius: 4px;
background: #007bff;
color: white;
}
.button--danger {
background: #dc3545;
}
}
/* utilities 레이어 (가장 높은 우선순위) */
@layer utilities {
.mt-4 { margin-top: 1rem; }
.flex { display: flex; }
.hidden { display: none; }
}
/* 레이어 외부 스타일은 모든 레이어보다 우선순위 높음 */
.button { background: green; } /* utilities보다도 우선 적용 *//* 외부 라이브러리를 레이어에 포함 */
@import url('tailwindcss/base') layer(tailwind-base);
@import url('tailwindcss/components') layer(tailwind-components);
@import url('tailwindcss/utilities') layer(tailwind-utilities);
/* 내 커스텀 스타일은 레이어 외부에 — 항상 Tailwind보다 우선 */
.my-component {
background: coral; /* Tailwind 클래스와 충돌해도 이게 적용됨 */
}:has() 선택자 — 드디어 부모 선택 가능
:has()는 '특정 자식이나 형제를 포함하는 요소'를 선택하는 CSS 선택자다. 기존에는 JavaScript로 처리해야 했던 수많은 UI 패턴을 순수 CSS로 구현할 수 있게 되었다.
브라우저 지원: Chrome 105+, Safari 15.4+, Firefox 121+ (2023년 말 완전 지원)
/* 이미지를 포함한 카드는 다르게 스타일링 */
.card:has(img) {
padding: 0; /* 이미지가 있으면 패딩 제거 */
}
/* 체크된 체크박스의 부모 레이블 강조 */
label:has(input:checked) {
color: #007bff;
font-weight: bold;
}
/* 폼 검증: 유효하지 않은 입력이 있는 폼 */
form:has(:invalid) .submit-button {
opacity: 0.5;
pointer-events: none;
}
/* 빈 리스트를 포함한 섹션 숨기기 */
section:has(ul:empty) {
display: none;
}
/* 네비게이션이 열린 경우 body 스크롤 잠금 */
body:has(.nav-menu.open) {
overflow: hidden;
}
/* 첫 번째 자식이 h2인 경우만 스타일 적용 */
article:has(> h2:first-child) {
border-left: 4px solid #007bff;
padding-left: 1rem;
}/* 실전 예제: 다크 모드 토글 (JavaScript 없이) */
:root:has(#dark-mode-toggle:checked) {
--bg-color: #1a1a2e;
--text-color: #e0e0e0;
--card-bg: #16213e;
}
body {
background: var(--bg-color, #ffffff);
color: var(--text-color, #333333);
}
/* 숨겨진 체크박스 + :has()로 다크 모드 구현 */
/* HTML: */
/* HTML: */View Transitions API — 부드러운 페이지 전환
View Transitions API는 DOM 상태 변화 시 부드러운 전환 애니메이션을 제공하는 Web API다. SPA와 MPA 모두 지원하며, CSS로 전환 효과를 커스터마이징할 수 있다.
브라우저 지원 (2025년 기준): 동일 문서(SPA): Chrome 111+, Firefox 133+, Safari 18+ (Baseline Newly Available, 2025년 10월) / 교차 문서(MPA): Chrome 126+, Safari 18.2+ (Firefox 미지원)
// 기본 사용법 - SPA 페이지 전환
function navigateTo(newContent) {
// View Transition API 미지원 브라우저 대비 점진적 향상
if (!document.startViewTransition) {
document.getElementById('main').innerHTML = newContent;
return;
}
document.startViewTransition(() => {
// DOM 업데이트 — 자동으로 old와 new 상태 캡처
document.getElementById('main').innerHTML = newContent;
});
}
// Promise 기반 — 전환 완료 후 후속 처리
async function navigateWithCallback(url) {
const response = await fetch(url);
const html = await response.text();
const transition = document.startViewTransition(() => {
document.body.innerHTML = html;
});
await transition.finished;
console.log('전환 완료');
}/* 기본 전환 효과 커스터마이징 */
::view-transition-old(root) {
animation: 300ms ease-out fadeOut;
}
::view-transition-new(root) {
animation: 300ms ease-in fadeIn;
}
@keyframes fadeOut {
to { opacity: 0; transform: translateX(-30px); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(30px); }
}
/* 특정 요소에 view-transition-name 지정 (공유 요소 전환) */
.hero-image {
view-transition-name: hero-image;
}
.article-image {
view-transition-name: hero-image; /* 같은 이름으로 연결 */
}
/* 공유 요소 전환 효과 */
::view-transition-group(hero-image) {
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.3, 0, 0, 1);
}/* MPA(멀티 페이지 앱) 교차 문서 전환 */
/* 모든 페이지에 추가 */
@view-transition {
navigation: auto; /* 동일 출처 내 모든 내비게이션에 전환 적용 */
}
/* 슬라이드 전환 효과 */
@media (prefers-reduced-motion: no-preference) {
::view-transition-old(root) {
animation: 250ms slide-out-left;
}
::view-transition-new(root) {
animation: 250ms slide-in-right;
}
}
/* 접근성: 움직임 감소 선호 시 페이드만 */
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.01ms;
}
}CSS Popover API — 네이티브 팝오버
HTML popover 속성과 CSS를 활용하면 JavaScript 없이 툴팁, 드롭다운, 모달을 구현할 수 있다. Anchor Positioning API와 결합하면 더욱 강력한 포지셔닝이 가능하다.
<!-- popover 속성으로 네이티브 팝오버 -->
<button popovertarget="my-popover">팝오버 열기</button>
<div id="my-popover" popover>
<p>이것이 팝오버 내용입니다.</p>
<button popovertarget="my-popover" popovertargetaction="hide">닫기</button>
</div>/* popover 스타일링 */
[popover] {
/* 기본 브라우저 스타일 초기화 후 커스텀 */
border: none;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
/* 등장 애니메이션 */
opacity: 0;
transform: translateY(-10px);
transition: opacity 200ms, transform 200ms,
display 200ms allow-discrete,
overlay 200ms allow-discrete;
}
[popover]:popover-open {
opacity: 1;
transform: translateY(0);
}
/* 진입 시작 상태 (allow-discrete와 함께 사용) */
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: translateY(-10px);
}
}