# 03. 성능 · 브라우저 함정 · 폴백 > "SVG 필터는 예쁘다"와 "SVG 필터를 프로덕션에 넣어도 된다"는 다른 문제다. 이 문서는 후자를 위한 것이다. --- ## 1. 비용 모델 — 무엇이 비싼가 ### 1.1 기본 공식 ``` 프레임당 비용 ≈ (필터 영역 픽셀 수) × (프리미티브 수 × 프리미티브별 픽셀당 연산) × (재계산 빈도) ``` 세 항 모두가 곱해진다. 하나만 줄여도 효과가 크고, 셋 다 방치하면 폭발한다. ### 1.2 프리미티브별 비용 등급 | 등급 | 프리미티브 | 픽셀당 연산 | 비고 | |---|---|---|---| | **매우 저렴** | `feOffset`, `feFlood`, `feMerge`, `feTile` | 복사/상수 | 사실상 공짜 | | **저렴** | `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feComposite` | 산술 수 회 | 마음껏 써도 됨 | | **보통** | `feGaussianBlur`, `feDropShadow` | 분리형 컨볼루션 (반경 비례) | 반경이 커지면 급증 | | **비쌈** | `feMorphology`, `feDisplacementMap` | 이웃 탐색 / 랜덤 액세스 | 캐시 미스 다발 | | **매우 비쌈** | `feTurbulence`, `feConvolveMatrix`, `feDiffuseLighting`, `feSpecularLighting` | Perlin 반복 / N×N 커널 / 노멀 계산 | **애니메이션 금지 후보** | `feTurbulence`는 별도로 언급할 가치가 있다. Firefox 버그 트래커에 "far too slow"로 15년 넘게 올라와 있고, 최신 노트북에서도 큰 면적에 걸면 CPU 코어를 즉시 포화시킨다는 보고가 있다. **`numOctaves`가 1 늘어날 때마다 노이즈 계산이 한 층씩 더 쌓인다.** 5 이상은 시각적 이득 대비 손해다. ### 1.3 필터 영역이 곱해지는 방식 ``의 `width`/`height`는 **처리할 픽셀 수를 직접 결정**한다. | 필터 영역 | 원본 대비 픽셀 수 | |---|---| | `100% × 100%` | 1.00× | | `120% × 120%` (기본값) | 1.44× | | `160% × 160%` | 2.56× | | `180% × 180%` | 3.24× | | `200% × 200%` | 4.00× | > 노이즈 오버레이처럼 영역 확장이 필요 없는 필터는 **반드시 `x="0%" y="0%" width="100%" height="100%"`** 로 줄여라. 그것만으로 30% 절약이다. ### 1.4 재계산이 일어나는 시점 필터 결과는 요소가 변하지 않는 한 캐시된다. 아래 상황에서 캐시가 무효화된다. | 상황 | 재계산 | |---|---| | 필터 어트리뷰트 변경 (`baseFrequency`, `scale`, `seed`, `stdDeviation`) | ✅ 전체 | | `flood-color` / `lighting-color` 변경 | ✅ 전체 (단, 형태 계산은 재사용 가능) | | 필터 걸린 요소의 **자식이 변함** (텍스트, 애니메이션) | ✅ 전체 | | 필터 걸린 요소의 **크기 변경** | ✅ 전체 | | 필터 걸린 요소의 `transform` / `opacity` 변경 | ⚠️ 엔진·상황에 따라 다름. 합성 레이어로 승격되면 재사용되지만, 그렇지 않으면 매 프레임 재계산 | | `backdrop-filter`에서 **배경이 스크롤/변화** | ✅ 매 프레임 | > **가장 흔한 성능 사고**: 필터 걸린 요소를 `transform`으로 움직이는 것. 브라우저가 캐시된 레이어를 합성하는 대신 매 프레임 필터 전체를 다시 돌릴 수 있다. **필터와 모션은 서로 다른 요소에 분리하라.** ```html
``` ### 1.5 `will-change: filter`를 쓰지 마라 (대부분의 경우) - `will-change: filter`는 요소를 별도 합성 레이어로 승격시키고 **메모리를 상시 점유**한다. - 필터 자체는 여전히 CPU/GPU에서 재계산된다 — 승격이 필터 연산을 없애주지 않는다. - 다수 요소에 걸면 레이어 폭발로 오히려 느려진다. - **예외**: 짧은 인터랙션 직전에 JS로 붙였다가 끝나면 제거하는 패턴은 유효하다. ```js el.addEventListener('pointerenter', () => { el.style.willChange = 'filter'; }); el.addEventListener('transitionend', () => { el.style.willChange = 'auto'; }); ``` ### 1.6 하드웨어 가속의 현실 - 엔진별로 가속하는 필터가 다르다. Chrome이 가속하는 것, Firefox가 가속하는 것이 서로 다르고, 일부 안드로이드 GPU는 아무것도 가속하지 못한다. - `feTurbulence`는 하드웨어 가속에 친화적이지 않은 것으로 알려져 있다. - **Safari는 CSS 단축 필터(`blur()`, `drop-shadow()` 등)에서는 잘 동작하지만 SVG ``에서는 문제가 잦다.** - Firefox에는 "하드웨어 가속을 켜면 대부분의 CSS/SVG 필터가 동작하지 않는다"는 장기 미해결 버그가 보고되어 있다. **결론**: 가속을 전제로 설계하지 마라. **CPU에서 돌아도 부드러운 수준**으로 설계하고, 가속은 보너스로 취급하라. --- ## 2. CSS 필터 vs SVG 필터 — 선택 기준 | 판단 | 선택 | |---|---| | blur / brightness / contrast / saturate / hue-rotate / drop-shadow 만 필요 | **CSS 단축 필터** — 더 빠르고, 더 잘 가속되고, Safari에서 안전하며, 값 보간 애니메이션이 가능 | | 노이즈, 변위, 채널 분리, 알파 임계, 조명, 커스텀 톤커브 | **SVG 필터** — 대안이 없다 | | 애니메이션이 필요한 값 | CSS 단축 필터는 **보간된다**. SVG 필터 어트리뷰트는 SMIL/JS만 가능 | **혼합 사용이 가능하다** — 무거운 SVG 필터는 정적으로 두고, 값이 변하는 부분만 CSS로 처리하는 것이 최적이다. ```css .card { /* SVG 필터는 고정, CSS 필터만 트랜지션 */ filter: url(#texture) brightness(1); transition: filter 240ms ease; } .card:hover { filter: url(#texture) brightness(1.15) saturate(1.2); } ``` > `filter` 목록에서 `url()`은 그대로 두고 뒤의 CSS 함수만 바뀌면 브라우저가 그 부분만 보간한다. **CSS 필터로 대체 가능한지 먼저 확인하라.** | SVG 필터 조합 | CSS 대체 | |---|---| | `feColorMatrix type="saturate" values="0"` | `filter: grayscale(1)` (sRGB로 동작) | | `feGaussianBlur` + `feOffset` + `feFlood` + `feMerge` | `filter: drop-shadow(x y blur color)` | | `feColorMatrix type="hueRotate"` | `filter: hue-rotate(deg)` | | 큰 소프트 글로우 | **`radialGradient` 또는 `box-shadow`가 압도적으로 싸다** | --- ## 3. `backdrop-filter` 함정 ### 3.1 지원 현황 | 기능 | Chromium | Safari | Firefox | |---|---|---|---| | `backdrop-filter: blur()` 등 CSS 함수 | ✅ | ✅ (`-webkit-` 접두사 필요했던 시기 있음) | ✅ | | **`backdrop-filter: url(#svgFilter)`** | ✅ | ❌ | ❌ | `backdrop-filter`에 SVG 필터를 넣는 것은 **CSS 스펙에 없는 Chromium 확장**이다. Liquid Glass류 효과가 Chrome에서만 보이는 이유가 이것이다. Firefox에는 기능 요청이 접수되어 있고(2025), `mdn/browser-compat-data`에도 관련 이슈가 열려 있다. ### 3.2 기능 감지가 신뢰할 수 없다 ⚠️ ```js CSS.supports('backdrop-filter', 'url(#x)') // Chromium: true (검증됨) CSS.supports('backdrop-filter', 'url(#x) brightness(1.1)') // Chromium: true (검증됨) ``` `CSS.supports`와 `@supports`는 **값을 파싱할 수 있는지**만 검사한다. Firefox/Safari도 `url()`을 유효한 `` 문법으로 파싱하므로 `true`를 반환할 가능성이 높다 — 그런데 렌더링은 하지 않는다. **권장 전략: 감지하지 말고, 계단식으로 쌓아라.** ```css /* 1단계: 모든 브라우저에서 그럴듯한 기본값 */ .glass { backdrop-filter: blur(14px) saturate(1.4); background: rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.28); box-shadow: inset 0 1px 1px rgba(255,255,255,.6), 0 10px 30px rgba(0,0,0,.4); } /* 2단계: 굴절을 지원하면 덮어쓴다. 지원 안 하는 엔진에서는 이 선언이 무효 처리되어 1단계가 남는다. */ @supports (backdrop-filter: url(#lens)) { .glass { backdrop-filter: url(#lens) brightness(1.06) saturate(1.25); } } ``` > `@supports`가 통과해버리는 엔진에서는 `backdrop-filter`가 통째로 무시되어 유리 배경만 남는다. 그래서 **`background`와 `box-shadow`만으로도 유리처럼 보이게** 설계해두는 것이 핵심이다. **확실히 가르고 싶다면 렌더 프로브를 써라** (미검증 — 로직 참고용): ```js async function supportsBackdropSVGFilter() { if (!('CSS' in window) || !CSS.supports('backdrop-filter', 'blur(1px)')) return false; // Chromium 계열만 확실히 지원한다는 사실에 기대는 실용적 판단 const ua = navigator.userAgent; const isChromium = /Chrome|Chromium|Edg/.test(ua) && !/OPR|Firefox/.test(ua); const isAppleWebKit = /Safari/.test(ua) && !/Chrome|Chromium|Edg/.test(ua); return isChromium && !isAppleWebKit; } ``` > UA 스니핑은 최후의 수단이다. 실패 시 폴백이 충분히 좋다면 감지 자체를 하지 않는 편이 낫다. ### 3.3 크기 불일치 `backdrop-filter`의 필터 영역은 요소 크기에 **자동으로 맞춰지지 않는다.** `feImage` 변위맵의 `width/height`가 요소와 다르면 굴절이 어긋나거나 잘린다. - 크기별로 별도 맵을 만들어라. - 반응형으로 크기가 변하는 요소에는 이 기법을 쓰지 마라. - 크기 변경 애니메이션은 매 프레임 맵 재계산 = 최악의 시나리오. ### 3.4 `backdrop-filter`는 배경 합성을 강제한다 배경이 스크롤하거나 변하면 **매 프레임 배경을 다시 캡처해 필터를 돌린다.** 뷰포트 전체를 덮는 `backdrop-filter`는 스크롤 성능을 파괴한다. 작은 칩/툴바/모달 헤더에만 써라. --- ## 4. 좌표계·색공간 함정 (요약과 진단) §01에 상세히 있고, 여기서는 **증상 → 원인** 진단표로 정리한다. | 증상 | 원인 | 처방 | |---|---|---| | 효과가 사각형으로 잘림 | 필터 영역이 좁음 | `x/y/width/height` 확대 | | 색이 물빠진 듯 밝고 채도가 낮음 | linearRGB 기본값 | `color-interpolation-filters="sRGB"` | | 듀오톤 색이 지정한 hex와 다름 | 위와 동일 | 위와 동일 | | `stdDeviation="10"`인데 화면이 사라짐 | `primitiveUnits="objectBoundingBox"` | 값을 0~1 스케일로 다시 계산 | | 변위가 이상한 방향으로 감 | `xChannelSelector`/`yChannelSelector` 기본값이 `A` | `R`/`G` 명시 | | 색이 전부 흰색으로 날아감 | `feColorMatrix` 값에 0~255를 씀 | 255로 나눠 0~1로 | | 필터 영역 전체가 단색 사각형 | `feFlood`/조명 결과를 클리핑 안 함 | `feComposite operator="in"` + `SourceAlpha` | | 분기 배선이 무시됨 | `in`을 명시하지 않아 직전 출력이 들어감 | 양쪽 모두 `in` 명시 | | 노이즈가 컬러 스노우로 보임 | feTurbulence의 알파도 노이즈 | `feColorMatrix`로 A를 1 상수화 | | 노이즈에 격자 사각형이 보임 | `seed`가 514/1977/2337/4777/8032/9615 등 | seed 변경 | | 요소가 통째로 사라짐 | `url(#id)` 오타 (레거시 엔진) | ID 확인 | --- ## 5. CSS 레이아웃 부작용 (놓치기 쉬운 것) `filter`가 `none`이 아닌 값을 가지면 그 요소는: 1. **새 스태킹 컨텍스트**를 만든다 → `z-index` 계층이 갇힌다. 2. **`position: fixed` / `absolute` 자손의 컨테이닝 블록이 된다** → 하위의 `position: fixed` 모달이 뷰포트가 아니라 그 요소 기준으로 배치된다. `backdrop-filter`, `perspective`, `will-change: transform`, `contain: layout|paint|strict|content`, `content-visibility: auto`도 동일한 부작용을 갖는다. > 이 동작은 호환성 때문에 표준화된 것이다. 모바일 WebKit이 먼저 하고 Chrome 22에서 따라갔으며, 2015년 워킹그룹 결의를 거쳐 Firefox가 마지막으로 합류했다. **실무 영향** ```html
``` **회피** - 필터를 콘텐츠 컨테이너가 아니라 **별도의 표면(surface) 레이어**에만 건다 (레시피 부록 B의 패턴). - 오버레이/모달은 Portal로 `` 직속에 렌더한다. - DevTools는 "이 요소가 컨테이닝 블록을 만든다"를 시각적으로 표시해주지 않는다. 증상(모달 위치가 이상함)에서 역추적하는 수밖에 없다. --- ## 6. 브라우저별 알려진 이슈 ### 6.1 Safari / WebKit | 이슈 | 영향 | |---|---| | `backdrop-filter: url()` 미지원 | Liquid Glass 계열 전부 폴백 필요 | | `feImage`로 **외부 SVG 파일** 참조 미지원 | data URI로 우회 | | `feImage` **fragment 참조**(`href="#id"`)는 Safari **만** 지원 | 크로스브라우저에서는 쓸 수 없음 | | `feImage`가 체인에 있으면 **HTML 요소에 필터 적용 실패** 보고 | SVG 콘텐츠에만 적용하거나 폴백 | | 대형 요소에 필터를 걸면 고해상도에서 **아예 렌더되지 않는** 경우 보고 | 필터 적용 면적 제한 | | SVG ``보다 CSS 단축 필터가 훨씬 안정적 | 가능하면 CSS 단축 사용 | | `feGaussianBlur`의 `edgeMode`는 **Safari만 지원** | 크로스브라우저에서는 사용 불가 | ### 6.2 Firefox | 이슈 | 영향 | |---|---| | `backdrop-filter: url()` 미지원 (기능 요청 계류 중) | 폴백 필요 | | `feTurbulence` 성능 이슈 장기 보고 | 큰 면적 금지 | | 하드웨어 가속 활성 시 필터 미동작 버그 보고 | 사용자 환경에 따라 효과가 안 보일 수 있음 | | `feComposite operator="arithmetic"` 시각 아티팩트 버그 보고 | 대안 연산자 고려 | | 블러 반경 상한(≈100px) | 큰 블러는 다른 방법으로 | | `feGaussianBlur edgeMode` 미지원 | — | ### 6.3 모바일 전반 - 큰 면적 필터 + 애니메이션 = **배터리 소모 + 열 스로틀링**. 스로틀링이 걸리면 페이지 전체가 느려진다. - 일부 안드로이드 GPU는 필터 가속을 전혀 못 한다. - 저가 기기에서 `feTurbulence` 애니메이션은 사실상 사용 불가. - GSAP 포럼 등에서 "데스크톱은 멀쩡한데 모바일에서 feTurbulence가 죽는다"는 사례가 반복 보고된다. ### 6.4 feTurbulence seed 아티팩트 특정 `seed` 값에서 노이즈에 **격자 사각형**이 생긴다. 보고된 값: `514, 1977, 2337, 4777, 8032, 9615` 등. - 원인은 SVG 스펙의 참조 구현에 있는 정수 나눗셈 버그다. gradient 테이블의 두 값이 모두 0이 되는 경우가 생긴다. - Chrome, Firefox, Batik, Inkscape 등 독립 구현들이 스펙을 그대로 따랐기 때문에 **모두 동일하게 재현**된다. - **처방**: seed는 무작위로 생성하지 말고, 1~50 정도의 작은 값 중 눈으로 확인한 것을 하드코딩하라. --- ## 7. 접근성 ### 7.1 `prefers-reduced-motion` 애니메이션되는 필터는 전정계 장애가 있는 사용자에게 실제로 불편을 준다. **모든 애니메이션 필터에 감소 모션 처리를 넣어라.** ```css /* 방법 1: 필터 자체를 끈다 */ @media (prefers-reduced-motion: reduce) { .wave, .squiggle-art, .glitch-box { filter: none; } } /* 방법 2: 정적 버전으로 대체 */ @media (prefers-reduced-motion: reduce) { .pulse-card { filter: none; box-shadow: 0 0 0 3px #22d3ee; } } ``` **SMIL은 CSS 미디어쿼리로 끌 수 없다.** JS로 처리해야 한다. ```js const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); function applyMotionPreference() { const svgRoot = document.querySelector('#filterDefs'); if (!svgRoot) return; if (mq.matches) { // SMIL 타임라인 정지 if (typeof svgRoot.pauseAnimations === 'function') svgRoot.pauseAnimations(); } else { if (typeof svgRoot.unpauseAnimations === 'function') svgRoot.unpauseAnimations(); } } applyMotionPreference(); mq.addEventListener('change', applyMotionPreference); ``` > **미검증**: `pauseAnimations()`/`unpauseAnimations()`는 `SVGSVGElement`의 표준 메서드지만 이 세션에서 실측하지 않았다. SMIL을 아예 쓰지 않고 JS 타이머로 어트리뷰트를 바꾸는 방식(레시피 11)이 제어가 확실하다. > ⚠️ SVG를 ``나 CSS `background-image`로 넣으면 브라우저가 샌드박스 처리해 **내부의 `prefers-reduced-motion` 미디어쿼리가 평가되지 않는다.** 인라인 SVG를 쓰거나, 애니메이션을 바깥 CSS에서 제어하라. ### 7.2 텍스트 가독성 - 왜곡 필터를 텍스트에 걸면 **판독성이 떨어진다.** `feDisplacementMap scale`은 텍스트에서 4~8을 넘기지 마라. - 필터는 **DOM 텍스트를 그대로 유지**한다 — 선택·복사·검색·스크린리더 접근은 살아있다. 이것이 이미지로 굽는 것보다 나은 결정적 이유다. - 듀오톤/포스터화로 **대비가 낮아질 수 있다.** 텍스트 위에 적용한다면 WCAG 대비비를 실제 렌더 결과로 다시 측정하라. - 색수차(RGB split)는 난독증·시각 피로가 있는 사용자에게 특히 읽기 어렵다. 본문 텍스트에는 쓰지 마라. ### 7.3 필터 정의 SVG의 접근성 ```html ``` - `aria-hidden="true"`: 스크린리더가 빈 그래픽으로 읽지 않게. - `focusable="false"`: 구형 IE/Edge에서 탭 순서에 들어가지 않게. - `width="0" height="0"` + `position:absolute`: 레이아웃에 영향 없음. --- ## 8. 폴백 · 저사양 대응 전략 ### 8.1 계층적 설계 (권장) ``` Layer 0 — 필터 없이도 완성된 디자인 (색, 그림자, 그라디언트) Layer 1 — CSS 단축 필터로 얹는 개선 (blur, saturate, drop-shadow) Layer 2 — SVG 필터로 얹는 개선 (질감, 굴절, 색수차) ``` 각 레이어가 없어도 아래 레이어가 완결되어야 한다. **필터를 지웠을 때 깨지는 디자인은 잘못 설계된 것이다.** ### 8.2 저사양 기기 감지 ```js function shouldDisableHeavyFilters() { // 1) 사용자 선호 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return true; // 2) 하드웨어 힌트 (모든 브라우저가 제공하지는 않음) const cores = navigator.hardwareConcurrency; // Chromium/Firefox const memory = navigator.deviceMemory; // Chromium only, GB 단위 if (typeof cores === 'number' && cores <= 4) return true; if (typeof memory === 'number' && memory <= 4) return true; // 3) 네트워크 절약 모드 (Chromium) const conn = navigator.connection; if (conn && conn.saveData) return true; // 4) 좁은 화면 = 대체로 모바일 if (window.matchMedia('(max-width: 640px)').matches) return true; return false; } if (shouldDisableHeavyFilters()) { document.documentElement.classList.add('reduce-effects'); } ``` ```css /* 무거운 필터를 값싼 대체물로 교체 */ .reduce-effects .paper-surface { filter: none; background: #e8e0d0; } .reduce-effects .squiggle-art { filter: none; } .reduce-effects .prism-pill { backdrop-filter: blur(10px) saturate(1.4); } .reduce-effects .grainy::after { opacity: 0.25; } /* 그레인은 싸니 약하게 유지 */ ``` > `navigator.hardwareConcurrency`/`deviceMemory`는 Chromium에서 실측 확인했다(각각 24, 32 반환). Safari는 `deviceMemory`를 제공하지 않으므로 `typeof` 가드가 필수다. ### 8.3 뷰포트 밖 / 백그라운드 탭에서 정지 애니메이션 필터에는 필수다. ```js function gateAnimation(targetEl, start, stop) { let visible = false; let focused = !document.hidden; function sync() { (visible && focused) ? start() : stop(); } if ('IntersectionObserver' in window) { new IntersectionObserver(entries => { visible = entries[0].isIntersecting; sync(); }, { threshold: 0 }).observe(targetEl); } else { visible = true; } document.addEventListener('visibilitychange', () => { focused = !document.hidden; sync(); }); sync(); } ``` ### 8.4 "구워서 쓰기" — 가장 확실한 최적화 정적 텍스처(종이 질감, 그레인 패턴, 리소 그레인)는 **한 번 렌더해서 PNG/WebP로 저장**하고 `background-image`로 쓰는 것이 압도적으로 싸다. - 필터 체인이 아무리 길어도 런타임 비용은 0이 된다. - 해상도 독립성을 잃지만, 노이즈/텍스처는 애초에 해상도 독립성이 필요 없다. - 512×512 타일을 만들어 `background-repeat`로 깔면 파일도 작다. - CSS-Tricks의 패턴 생성 글에서도 복잡한 필터 패턴은 "JPEG/PNG로 서빙해 사용자 CPU를 아끼라"고 권한다. **data URI vs 요소 필터** — 이미 §02 레시피 02/03에서 쓴 방식이다. `background-image: url("data:image/svg+xml,...")`는 **한 번만 래스터화되어 캐시**되므로, 같은 노이즈를 `filter: url(#noise)`로 거는 것보다 훨씬 싸다. **노이즈는 언제나 data URI 배경으로.** --- ## 9. 측정과 디버깅 ### 9.1 Chrome DevTools 1. **Performance 패널** → 녹화 → "Rendering" 트랙에서 Paint/Composite 시간 확인. 필터가 문제면 Paint가 길어진다. 2. **Rendering 탭** → "Paint flashing" 켜기 → 필터 영역이 매 프레임 초록색으로 깜빡이면 재계산 중이다. 3. **Rendering 탭** → "Layer borders"로 합성 레이어 승격 여부 확인. 4. **CPU throttling 4×/6×** 로 저사양 기기 시뮬레이션. 이것이 가장 현실적인 테스트다. ### 9.2 A/B 비교 방법 필터 하나가 얼마나 비싼지 알고 싶으면 DevTools 콘솔에서 껐다 켜본다. ```js document.querySelectorAll('.effect').forEach(el => { el.style.filter = 'none'; }); ``` FPS가 확 오르면 그 필터가 범인이다. ### 9.3 오프라인 도구 | 도구 | 용도 | |---|---| | [yoksel/svg-filters](https://yoksel.github.io/svg-filters/) | 필터 체인을 GUI로 만들고 코드 추출 | | [yoksel/svg-gradient-map](https://yoksel.github.io/svg-gradient-map/) | 듀오톤/트라이톤 `tableValues` 자동 생성 | | [fffuel nnnoise](https://www.fffuel.co/nnnoise/) | 노이즈 텍스처 SVG 생성 | --- ## 10. 프로덕션 체크리스트 배포 전에 이 목록을 통과시켜라. **성능** - [ ] 필터 영역(`x/y/width/height`)이 필요한 최소 크기인가 - [ ] `numOctaves`가 4를 넘지 않는가 - [ ] 애니메이션되는 필터의 적용 면적이 400×400px 이하인가 - [ ] 애니메이션 필터에 IntersectionObserver / visibilitychange 게이팅이 있는가 - [ ] 필터와 `transform` 애니메이션이 같은 요소에 걸려 있지 않은가 - [ ] `will-change: filter`를 남발하지 않았는가 - [ ] 정적 텍스처를 런타임 필터로 매번 계산하고 있지 않은가 (→ data URI 배경 또는 PNG) - [ ] CPU throttling 4×에서 스크롤이 부드러운가 **호환성** - [ ] `color-interpolation-filters`를 의도적으로 설정했는가 - [ ] `feDisplacementMap`의 채널 셀렉터를 명시했는가 - [ ] `feImage`를 쓴다면 data URI인가 (외부 파일/fragment 아님) - [ ] `backdrop-filter: url()`에 의존한다면 폴백만으로도 완성도가 있는가 - [ ] `edgeMode`, `kernelUnitLength`, `BackgroundImage`, `FillPaint`를 쓰지 않았는가 - [ ] `feTurbulence` seed가 알려진 문제 값이 아닌가 - [ ] Safari / Firefox / 모바일 실기기에서 눈으로 확인했는가 **접근성** - [ ] 애니메이션 필터에 `prefers-reduced-motion` 처리가 있는가 - [ ] 필터 정의 SVG에 `aria-hidden="true"`가 있는가 - [ ] 텍스트에 필터를 걸었다면 판독성과 대비비를 확인했는가 - [ ] 필터가 전부 꺼져도 디자인이 성립하는가 **레이아웃** - [ ] 필터를 건 요소 안에 `position: fixed` 자손이 없는가 - [ ] 필터로 인한 스태킹 컨텍스트가 `z-index` 계층을 깨지 않는가 - [ ] 필터 컨테이너 안에 본문 텍스트가 들어가 있지 않은가