모던 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);
  }
}