@supports - 안전하게 최신 CSS를 사용하는 법

최신 CSS 기능을 사용하고 싶은데 “구형 브라우저에서는 어떻게 하지?”라는 고민을 해보신 적 있나요?

.container {
  display: grid; /* IE에서는 안 되는데... */
  gap: 1rem;     /* 이것도 안 되는 브라우저가 있을 텐데... */
}

새로운 기능을 쓰면 레이아웃이 깨질까 봐 걱정되고, 그렇다고 구형 문법만 쓰자니 답답하죠. “모든 브라우저에서 작동하는지 어떻게 알지?”, “폴백은 어떻게 제공하지?”

@supports는 바로 이 문제를 해결합니다. 브라우저가 특정 CSS 기능을 지원하는지 확인하고, 지원할 때만 해당 스타일을 적용하는 조건문입니다.

왜 @supports가 필요한가?

웹 개발에서 브라우저 호환성은 영원한 숙제입니다. 하지만 “모든 브라우저에 맞추느라 최신 기능을 못 쓰는 것”과 “최신 기능 쓰다가 구형 브라우저 사용자를 포기하는 것” 사이에는 중간 지대가 있습니다.

실제 문제 상황

문제 1: 기능 지원 불확실성

/* 이 코드는 언제 깨질까요? */
.fancy-text {
  text-stroke: 2px blue; /* 일부 브라우저만 지원 */
}

결과:

  • ✅ 지원 브라우저: 외곽선
  • ❌ 미지원 브라우저: 아무 효과 없음 (다행히 레이아웃은 안 깨짐)

문제 2: 깨진 레이아웃

/* 이건 더 심각합니다 */
.layout {
  display: grid; /* IE11 미지원 */
  grid-template-columns: repeat(3, 1fr);
}

결과:

  • ✅ 최신 브라우저: 완벽한 그리드 레이아웃
  • ❌ IE11: 레이아웃이 완전히 깨짐 😱

문제 3: 접두사 지옥

/* 어떤 접두사가 필요한지 어떻게 알죠? */
.transform {
  -webkit-transform: rotate(45deg);
  -moz-transform: rotate(45deg);
  -ms-transform: rotate(45deg);
  transform: rotate(45deg);
}

고민:

  • “이 접두사 중에 어떤 게 실제로 필요하지?”
  • “표준 속성만 지원하는 브라우저는 어떻게 하지?”

생각해보면, 우리는 기능 지원 여부를 확인하는 방법이 필요합니다. JavaScript의 if문처럼 CSS에도 조건문이 있다면? 그게 바로 @supports입니다.

먼저, 기초부터 이해하기

@supports가 어떻게 작동하는지 이해하려면, 브라우저가 CSS를 어떻게 처리하는지 알아야 합니다.

브라우저의 기본 동작

CSS에서 브라우저가 모르는 속성을 만나면:

.element {
  display: flex;        /* ✅ 지원: 적용됨 */
  display: unknown-value; /* ❌ 미지원: 무시됨 */
  color: red;           /* ✅ 계속 파싱 */
}

특징:

  • 모르는 속성은 조용히 무시
  • 에러 없이 다음 속성으로 진행
  • 이를 “graceful degradation(우아한 성능 저하)”라고 부름

하지만 이 방식의 문제는 컨트롤할 수 없다는 점입니다. 지원 여부에 따라 다른 스타일을 적용하고 싶다면?

@supports의 등장

MDN 문서에 따르면, @supports“브라우저의 CSS 기능 지원 여부에 따른 CSS 선언을 지정”할 수 있게 합니다.

/* 기본 폴백 */
.container {
  display: block;
}

/* Grid 지원 시 업그레이드 */
@supports (display: grid) {
  .container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
  }
}

동작 방식:

  1. 브라우저가 display: grid 지원 여부 체크
  2. ✅ 지원하면: @supports 블록 내부 실행
  3. ❌ 미지원하면: 블록 전체 무시

기본 문법

@supports는 세 가지 핵심 패턴이 있습니다.

1. 단일 조건 (기본)

@supports (property: value) {
  /* 조건이 참일 때만 적용 */
}

예제:

/* ❌ 이렇게 쓰지 마세요 */
@supports display: grid {
  /* 괄호 없음 - 에러! */
}

/* ✅ 올바른 방법 */
@supports (display: grid) {
  .container {
    display: grid;
  }
}

주의: 괄호 ( )는 필수입니다!

2. NOT 연산자 (부정)

@supports not (property: value) {
  /* 지원하지 않을 때만 적용 */
}

예제:

/* Grid를 지원하지 않는 브라우저용 폴백 */
@supports not (display: grid) {
  .container {
    display: flex; /* Grid 대신 Flexbox 사용 */
  }
}

3. AND 연산자 (그리고)

@supports (property1: value1) and (property2: value2) {
  /* 둘 다 지원할 때만 적용 */
}

예제:

/* Flexbox와 gap을 둘 다 지원할 때만 */
@supports (display: flex) and (gap: 1rem) {
  .flex-container {
    display: flex;
    gap: 1rem; /* 최신 gap 속성 */
  }
}

왜 필요할까?

gap은 Grid에서 먼저 나왔지만, Flexbox에서는 나중에 추가되었습니다. display: flex만 지원하고 gap은 미지원인 브라우저가 있을 수 있죠.

4. OR 연산자 (또는)

@supports (property: value1) or (property: value2) {
  /* 둘 중 하나라도 지원하면 적용 */
}

예제:

/* 표준 또는 webkit 접두사 중 하나라도 지원 */
@supports (backdrop-filter: blur(10px)) or
          (-webkit-backdrop-filter: blur(10px)) {
  .modal-backdrop {
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px); /* Safari */
  }
}

복잡한 조건 조합

여러 연산자를 조합할 때는 괄호로 우선순위를 명확히 해야 합니다.

예제 1: (A and B) or C

@supports ((display: flex) and (gap: 1rem)) or (display: grid) {
  .layout {
    /* Flexbox+gap 또는 Grid 지원 시 */
  }
}

의미:

  • “Flexbox와 gap을 모두 지원” 또는
  • “Grid를 지원”

예제 2: not (A or B)

@supports not ((transform: rotate(45deg)) or
                (-webkit-transform: rotate(45deg))) {
  /* transform을 전혀 지원하지 않을 때 */
  .fallback {
    /* 대체 스타일 */
  }
}

❌ 잘못된 예: 괄호 없음

/* 이건 에러! */
@supports (display: flex) and (gap: 1rem) or (display: grid) {
  /* 우선순위가 불명확 */
}

MDN 문서 명시: “복잡한 조건 조합 시 괄호 필수”

실전 활용 예제

실무에서 자주 사용되는 패턴들을 살펴봅시다.

1. Grid 레이아웃 폴백

Before (위험한 코드)

/* Grid 미지원 브라우저에서 깨짐 */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
}

After (@supports 사용)

/* 기본: 모든 브라우저 */
.gallery {
  display: flex;
  flex-wrap: wrap;
}

.gallery > * {
  flex: 1 1 200px;
  margin: 0.5rem; /* gap 대신 margin */
}

/* Grid 지원: 업그레이드 */
@supports (display: grid) {
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 1rem;
  }

  .gallery > * {
    margin: 0; /* Grid gap 사용하므로 margin 제거 */
  }
}

효과:

  • ✅ IE11: Flexbox 레이아웃
  • ✅ 최신 브라우저: Grid 레이아웃
  • ✅ 모든 브라우저에서 깨지지 않음!

2. 스티키 포지션 폴백

/* 기본: fixed 사용 (약간 아쉬움) */
.header {
  position: fixed;
  top: 0;
  width: 100%;
}

/* sticky 지원: 더 나은 UX */
@supports (position: sticky) {
  .header {
    position: sticky;
    /* fixed와 달리 문서 흐름 유지 */
  }
}

차이점:

  • fixed: 항상 화면에 고정 (문서 흐름에서 벗어남)
  • sticky: 스크롤 시에만 고정 (더 자연스러움)

3. CSS 변수 폴백

/* 기본: 하드코딩 색상 */
.button {
  background: #3498db;
  color: white;
}

/* CSS 변수 지원: 테마 시스템 */
@supports (--custom: property) {
  :root {
    --primary-color: #3498db;
    --text-color: white;
  }

  .button {
    background: var(--primary-color);
    color: var(--text-color);
  }
}

Note: CSS 변수는 이제 대부분 지원되지만, 예시로서 패턴을 보여드립니다.

4. aspect-ratio 폴백

/* 기본: padding 해킹 */
.video-container {
  position: relative;
  padding-bottom: 56.25%; /* 16:9 비율 */
}

.video-container > iframe {
  position: absolute;
  width: 100%;
  height: 100%;
}

/* aspect-ratio 지원: 깔끔한 방법 */
@supports (aspect-ratio: 16 / 9) {
  .video-container {
    aspect-ratio: 16 / 9;
    padding-bottom: 0; /* 해킹 제거 */
  }

  .video-container > iframe {
    position: static; /* 더 간단 */
  }
}

5. :has() 선택자 폴백

/* 기본: JavaScript로 클래스 추가 필요 */
.card.has-image {
  display: grid;
}

/* :has() 지원: CSS만으로 해결 */
@supports selector(:has(img)) {
  .card:has(img) {
    display: grid;
  }
}

selector() 함수: 선택자 지원 여부를 체크합니다 (비교적 최신 기능).

6. 접두사 자동 처리

/* 표준 또는 접두사 중 하나라도 지원 */
@supports (text-stroke: 2px) or (-webkit-text-stroke: 2px) {
  .outlined-text {
    -webkit-text-stroke: 2px black;
    text-stroke: 2px black; /* 미래를 위한 표준 */
  }
}

JavaScript와의 연동

CSS만으로 부족할 때는 JavaScript를 사용할 수 있습니다.

CSS.supports() 메서드

MDN 문서에서 설명하는 두 가지 문법:

방법 1: 속성-값 분리

// 지원 여부 확인
if (CSS.supports('display', 'grid')) {
  console.log('Grid 지원!');
  // Grid 관련 JavaScript 코드
}

방법 2: 전체 표현식

// CSS @supports 문법과 동일
if (CSS.supports('(display: grid) and (gap: 1rem)')) {
  console.log('Grid + gap 둘 다 지원!');
}

실전 예제: 동적 클래스 추가

// 기능 지원 여부에 따라 클래스 추가
const supportsGrid = CSS.supports('display', 'grid');
const supportsGap = CSS.supports('gap', '1rem');

document.body.classList.toggle('supports-grid', supportsGrid);
document.body.classList.toggle('supports-gap', supportsGap);

CSS:

/* 기본 스타일 */
.layout {
  display: flex;
  flex-wrap: wrap;
}

/* Grid 지원 시 */
.supports-grid .layout {
  display: grid;
}

/* Grid + gap 둘 다 지원 시 */
.supports-grid.supports-gap .layout {
  gap: 1rem;
}

브라우저 호환성 체크

const features = [
  { name: 'Grid', check: '(display: grid)' },
  { name: 'Flexbox gap', check: '(gap: 1rem)' },
  { name: 'aspect-ratio', check: '(aspect-ratio: 16/9)' },
  { name: ':has()', check: 'selector(:has(a))' }
];

features.forEach(({ name, check }) => {
  const supported = CSS.supports(check);
  console.log(`${name}: ${supported ? '' : ''}`);
});

// 출력 예시 (최신 Chrome):
// Grid: ✅
// Flexbox gap: ✅
// aspect-ratio: ✅
// :has(): ✅

추가: 폰트 체크

MDN 문서에 따르면, @supports는 폰트 기술과 형식도 체크할 수 있습니다.

font-format() 함수

/* WOFF2 지원 시에만 로드 */
@supports font-format(woff2) {
  @font-face {
    font-family: "CustomFont";
    src: url("font.woff2") format("woff2");
  }
}

/* WOFF2 미지원 시 WOFF 폴백 */
@supports not font-format(woff2) {
  @font-face {
    font-family: "CustomFont";
    src: url("font.woff") format("woff");
  }
}

지원 형식: woff, woff2, truetype, opentype, embedded-opentype, svg

font-tech() 함수

/* 컬러 폰트 지원 체크 */
@supports font-tech(color-COLRv1) {
  .fancy-text {
    font-family: "Bungee Spice", fantasy;
  }
}

지원 기술:

  • color-COLRv1: 컬러 폰트 v1
  • color-SVG: SVG 기반 컬러 폰트
  • variations: 가변 폰트
  • features-opentype: OpenType 기능

함정과 주의사항

실제로 사용하면서 주의해야 할 점들입니다.

함정 1: 괄호 필수!

/* ❌ 에러 - 괄호 없음 */
@supports display: grid {
  /* 작동 안 함 */
}

/* ✅ 올바름 */
@supports (display: grid) {
  /* 작동함 */
}

함정 2: 값까지 정확히 체크

/* ❌ 이건 체크 안 됨 */
@supports (display) {
  /* 속성만 체크는 불가능 */
}

/* ✅ 속성과 값 모두 필요 */
@supports (display: grid) {
  /* 특정 값 지원 체크 */
}

함정 3: 접두사는 각각 체크

/* ❌ 이렇게 하면 표준만 체크 */
@supports (transform: rotate(45deg)) {
  .element {
    -webkit-transform: rotate(45deg); /* 체크 안 됨 */
    transform: rotate(45deg);
  }
}

/* ✅ OR로 접두사도 함께 체크 */
@supports (transform: rotate(45deg)) or
          (-webkit-transform: rotate(45deg)) {
  .element {
    -webkit-transform: rotate(45deg);
    transform: rotate(45deg);
  }
}

함정 4: 부분 지원 주의

어떤 속성은 기본은 지원하지만 특정 값은 미지원일 수 있습니다.

/* display는 지원하지만 display: grid는 미지원일 수 있음 */
@supports (display: grid) {
  /* grid 값을 정확히 체크 */
}

함정 5: 성능 고려

/* ❌ 모든 요소에 중복 체크 */
.element1 {
  @supports (display: grid) { /* 비효율적 */ }
}
.element2 {
  @supports (display: grid) { /* 중복 */ }
}

/* ✅ 한 번만 체크 */
@supports (display: grid) {
  .element1 { /* ... */ }
  .element2 { /* ... */ }
}

함정 6: 코드 위치 제한

MDN 명시: “코드 최상단 또는 다른 조건부 at-rule 내부에만 사용 가능”

/* ✅ 최상단 - OK */
@supports (display: grid) {
  .container { display: grid; }
}

/* ✅ @media 내부 - OK */
@media (min-width: 768px) {
  @supports (display: grid) {
    .container { display: grid; }
  }
}

/* ❌ 선택자 내부 - 불가능 */
.container {
  @supports (display: grid) {
    /* 에러! */
  }
}

실전 패턴: 점진적 향상

MDN 문서가 강조하는 “progressive enhancement(점진적 향상)” 전략입니다.

패턴 1: 모바일 우선 + 기능 향상

/* Level 1: 모든 브라우저 (모바일 포함) */
.layout {
  display: block;
}

.layout > * {
  margin-bottom: 1rem;
}

/* Level 2: Flexbox 지원 */
@supports (display: flex) {
  .layout {
    display: flex;
    flex-direction: column;
  }
}

/* Level 3: Grid 지원 */
@supports (display: grid) {
  @media (min-width: 768px) {
    .layout {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
      gap: 2rem;
    }

    .layout > * {
      margin-bottom: 0; /* Grid gap으로 대체 */
    }
  }
}

단계별 경험:

  • 구형 모바일: 세로 블록 레이아웃
  • Flexbox 브라우저: 유연한 레이아웃
  • Grid 브라우저 + 큰 화면: 최고의 그리드 경험

패턴 2: 기능 조합 체크

/* 여러 기능이 필요한 경우 */
@supports (display: grid) and
          (gap: 1rem) and
          (grid-template-areas: "a b") {
  .complex-layout {
    display: grid;
    gap: 1rem;
    grid-template-areas:
      "header header"
      "sidebar main"
      "footer footer";
  }
}

패턴 3: 폴백 체인

/* 3단계 폴백 */

/* Level 1: 기본 (모든 브라우저) */
.card {
  width: 100%;
  padding-bottom: 75%; /* 4:3 비율 해킹 */
}

/* Level 2: object-fit 지원 */
@supports (object-fit: cover) {
  .card img {
    object-fit: cover;
    width: 100%;
    height: 100%;
  }
}

/* Level 3: aspect-ratio 지원 (최선) */
@supports (aspect-ratio: 4 / 3) {
  .card {
    aspect-ratio: 4 / 3;
    padding-bottom: 0; /* 해킹 제거 */
  }
}

브라우저 호환성

MDN 문서에 따르면, @supports“Baseline Widely available” 등급입니다.

지원 현황

  • Chrome: 28+ (2013년 7월)
  • Firefox: 22+ (2013년 6월)
  • Safari: 9+ (2015년 9월)
  • Edge: 12+ (2015년 7월)

기준: 2015년 9월부터 광범위하게 사용 가능

미지원 브라우저 대응

/* @supports 자체를 지원하지 않는 브라우저는? */

/* 기본 스타일이 폴백 역할 */
.element {
  display: block; /* @supports 미지원 브라우저용 */
}

/* @supports 지원 브라우저만 실행 */
@supports (display: grid) {
  .element {
    display: grid; /* 업그레이드 */
  }
}

핵심: @supports 자체를 모르는 브라우저는 블록 전체를 무시하므로, 기본 스타일이 자동으로 폴백이 됩니다!

언제 사용할까?

✅ 사용하기 좋은 경우

  1. 최신 CSS 기능 도입
    • Grid, aspect-ratio, gap 등
    • 폴백이 필요한 경우
  2. 접두사 처리
    • backdrop-filter, text-stroke 등
    • 표준과 접두사 버전 모두 체크
  3. 레이아웃 전환
    • Flexbox → Grid 업그레이드
    • 구형 브라우저 지원 필수
  4. 실험적 기능 테스트
    • 새로운 선택자 (:has, :is 등)
    • 안전하게 점진적 도입

❌ 사용하지 않아도 되는 경우

  1. 광범위하게 지원되는 기능
    • Flexbox (IE11 제외하면 안전)
    • CSS 변수 (IE 버릴 수 있다면)
  2. 중요하지 않은 장식
    • 그림자, border-radius 등
    • 없어도 기능에 문제없음
  3. JavaScript로 해결 가능
    • 복잡한 조건 체크
    • 동적 기능 추가

완전한 실전 예제

모든 개념을 종합한 반응형 갤러리:

<!DOCTYPE html>
<html lang="ko">
<head>
  <style>
    /* ========================================
       Level 1: 기본 (모든 브라우저)
       ======================================== */
    .gallery {
      max-width: 1200px;
      margin: 0 auto;
      padding: 1rem;
    }

    .gallery-item {
      margin-bottom: 1rem;
      background: #f0f0f0;
      padding: 1rem;
    }

    /* ========================================
       Level 2: Flexbox 지원
       ======================================== */
    @supports (display: flex) {
      .gallery {
        display: flex;
        flex-wrap: wrap;
        gap: 1rem; /* Flexbox gap (비교적 최신) */
      }

      .gallery-item {
        flex: 1 1 calc(33.333% - 1rem);
        margin-bottom: 0;
      }
    }

    /* ========================================
       Level 2.5: Flexbox는 있는데 gap 없는 경우
       ======================================== */
    @supports (display: flex) and (not (gap: 1rem)) {
      .gallery {
        margin: -0.5rem; /* 음수 마진 해킹 */
      }

      .gallery-item {
        margin: 0.5rem;
      }
    }

    /* ========================================
       Level 3: Grid 지원 (최선)
       ======================================== */
    @supports (display: grid) {
      .gallery {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
        gap: 1rem;
        margin: 0; /* 음수 마진 해제 */
      }

      .gallery-item {
        margin: 0;
        flex: none; /* Flexbox 속성 리셋 */
      }
    }

    /* ========================================
       이미지 비율 처리
       ======================================== */
    .gallery-item img {
      width: 100%;
      height: 200px;
      object-fit: cover;
    }

    /* aspect-ratio 지원 시 더 나은 방법 */
    @supports (aspect-ratio: 16 / 9) {
      .gallery-item img {
        aspect-ratio: 16 / 9;
        height: auto; /* aspect-ratio가 높이 결정 */
      }
    }

    /* ========================================
       고급: :has() 선택자로 빈 갤러리 체크
       ======================================== */
    @supports selector(:has(img)) {
      .gallery:not(:has(.gallery-item)) {
        display: none;
      }

      .gallery:not(:has(.gallery-item))::before {
        content: "갤러리가 비어있습니다.";
        display: block;
        padding: 2rem;
        text-align: center;
        color: #999;
      }
    }
  </style>

  <script>
    // JavaScript로 기능 감지 및 로깅
    document.addEventListener('DOMContentLoaded', () => {
      const features = {
        'Flexbox': '(display: flex)',
        'Flexbox gap': '(gap: 1rem)',
        'Grid': '(display: grid)',
        'aspect-ratio': '(aspect-ratio: 16/9)',
        ':has()': 'selector(:has(a))'
      };

      console.log('=== 브라우저 기능 지원 ===');
      Object.entries(features).forEach(([name, query]) => {
        const supported = CSS.supports(query);
        console.log(`${name}: ${supported ? '' : ''}`);

        // body에 클래스 추가
        if (supported) {
          document.body.classList.add(`supports-${name.toLowerCase().replace(/[():]/g, '')}`);
        }
      });
    });
  </script>
</head>
<body>
  <div class="gallery">
    <div class="gallery-item">
      <img src="https://via.placeholder.com/300x200" alt="Image 1">
      <h3>항목 1</h3>
    </div>
    <div class="gallery-item">
      <img src="https://via.placeholder.com/300x200" alt="Image 2">
      <h3>항목 2</h3>
    </div>
    <div class="gallery-item">
      <img src="https://via.placeholder.com/300x200" alt="Image 3">
      <h3>항목 3</h3>
    </div>
    <div class="gallery-item">
      <img src="https://via.placeholder.com/300x200" alt="Image 4">
      <h3>항목 4</h3>
    </div>
    <div class="gallery-item">
      <img src="https://via.placeholder.com/300x200" alt="Image 5">
      <h3>항목 5</h3>
    </div>
    <div class="gallery-item">
      <img src="https://via.placeholder.com/300x200" alt="Image 6">
      <h3>항목 6</h3>
    </div>
  </div>
</body>
</html>

정리하며

@supports는 CSS의 조건문입니다. 최신 기능을 안전하게 사용하면서 구형 브라우저 호환성을 유지할 수 있게 해줍니다.

핵심 요약

  1. 목적: 브라우저의 CSS 기능 지원 여부 확인
  2. 기본 문법: @supports (property: value) { }
  3. 연산자: and, or, not
  4. JavaScript: CSS.supports() 메서드
  5. 전략: 점진적 향상 (기본 → 향상)

실무 체크리스트

  • 기본 폴백 먼저 작성
  • @supports로 최신 기능 추가
  • 괄호 ( ) 필수 확인
  • 복잡한 조건은 괄호로 우선순위 명시
  • 접두사도 or로 함께 체크
  • JavaScript CSS.supports() 활용
  • 실제 브라우저에서 테스트

마지막 조언

좋은 웹사이트는 모든 브라우저에서 동일하게 보일 필요는 없지만, 모든 브라우저에서 작동해야 합니다.

@supports점진적 향상의 핵심 도구입니다. 최신 브라우저 사용자에게는 최고의 경험을, 구형 브라우저 사용자에게는 기본적인 기능을 제공하세요.

“이 기능 써도 될까?”라는 고민이 들 때, 이제는 @supports로 안전하게 도입할 수 있습니다!

참고 자료

댓글