웹 디자인 파이프라인 스킬과 이를 5개 에이전트에 설치하는 CLI 를 담은 모노레포. 스킬 (packages/skill) - SKILL.md 261줄 + 참조 문서 16개 3,349줄. progressive disclosure 로 본문은 절차와 인덱스만, 지식은 references/ 로 분리 - 0~6단계 파이프라인. 규모에 따라 전체·연장·국소 세 경로로 분기 - 하드 게이트 12개는 grep·카운트로 검증 가능한 것만. 취향 판단은 제외 - 미학 프리셋 5종, AI 슬롭 지문 목록, 한글 조판 규칙, SVG 필터·three.js·인터랙티브 모션·HTML-in-Canvas 실전 지침 설치 CLI (packages/cli, packages/core) - npx designpaca 온보딩 TUI. Claude Code · Codex · Cursor · Windsurf · AGENTS.md - 매니페스트에 설치 시점 해시를 기록해 사용자가 고친 파일은 update 가 건너뛴다 - 타깃별로 본문의 references/ 경로를 실제 설치 위치로 재작성 - AGENTS.md 는 항상 로드되므로 본문 대신 303자 포인터만 주입 - Windsurf 는 12,000자 상한 초과 시 설치를 차단 배포 (build/ci, .forgejo/workflows) - 태그 v* → 검사·테스트·빌드 → npmjs 배포 + Forgejo 레지스트리 미러 → draft 릴리스 → Cloudflare Pages. 재실행 멱등 근거 (research/) - 약 250개 웹 소스 조사 결과와 도그푸딩 검증 2건. 스킬의 모든 수치는 여기서 나온다 테스트 22개 통과 (core 16 · cli 6)
24 KiB
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 필터 영역이 곱해지는 방식
<filter>의 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으로 움직이는 것. 브라우저가 캐시된 레이어를 합성하는 대신 매 프레임 필터 전체를 다시 돌릴 수 있다. 필터와 모션은 서로 다른 요소에 분리하라.
<!-- ❌ 같은 요소에 필터 + 모션 -->
<div class="blob" style="filter:url(#goo); animation: move 3s infinite"></div>
<!-- ✅ 바깥이 움직이고, 안쪽이 필터를 가진다 (혹은 그 반대) -->
<div class="mover" style="animation: move 3s infinite">
<div class="blob" style="filter:url(#goo)"></div>
</div>
1.5 will-change: filter를 쓰지 마라 (대부분의 경우)
will-change: filter는 요소를 별도 합성 레이어로 승격시키고 메모리를 상시 점유한다.- 필터 자체는 여전히 CPU/GPU에서 재계산된다 — 승격이 필터 연산을 없애주지 않는다.
- 다수 요소에 걸면 레이어 폭발로 오히려 느려진다.
- 예외: 짧은 인터랙션 직전에 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<filter>에서는 문제가 잦다. - 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로 처리하는 것이 최적이다.
.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 기능 감지가 신뢰할 수 없다 ⚠️
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()을 유효한 <filter-function> 문법으로 파싱하므로 true를 반환할 가능성이 높다 — 그런데 렌더링은 하지 않는다.
권장 전략: 감지하지 말고, 계단식으로 쌓아라.
/* 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만으로도 유리처럼 보이게 설계해두는 것이 핵심이다.
확실히 가르고 싶다면 렌더 프로브를 써라 (미검증 — 로직 참고용):
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이 아닌 값을 가지면 그 요소는:
- 새 스태킹 컨텍스트를 만든다 →
z-index계층이 갇힌다. position: fixed/absolute자손의 컨테이닝 블록이 된다 → 하위의position: fixed모달이 뷰포트가 아니라 그 요소 기준으로 배치된다.
backdrop-filter, perspective, will-change: transform, contain: layout|paint|strict|content, content-visibility: auto도 동일한 부작용을 갖는다.
이 동작은 호환성 때문에 표준화된 것이다. 모바일 WebKit이 먼저 하고 Chrome 22에서 따라갔으며, 2015년 워킹그룹 결의를 거쳐 Firefox가 마지막으로 합류했다.
실무 영향
<!-- 카드에 질감 필터를 걸었더니, 카드 안의 드롭다운이 화면 밖으로 못 나간다 -->
<div class="card" style="filter: url(#texture)">
<div class="dropdown" style="position: fixed">...</div> <!-- 카드 기준으로 배치됨 -->
</div>
회피
- 필터를 콘텐츠 컨테이너가 아니라 별도의 표면(surface) 레이어에만 건다 (레시피 부록 B의 패턴).
- 오버레이/모달은 Portal로
<body>직속에 렌더한다. - DevTools는 "이 요소가 컨테이닝 블록을 만든다"를 시각적으로 표시해주지 않는다. 증상(모달 위치가 이상함)에서 역추적하는 수밖에 없다.
6. 브라우저별 알려진 이슈
6.1 Safari / WebKit
| 이슈 | 영향 |
|---|---|
backdrop-filter: url() 미지원 |
Liquid Glass 계열 전부 폴백 필요 |
feImage로 외부 SVG 파일 참조 미지원 |
data URI로 우회 |
feImage fragment 참조(href="#id")는 Safari 만 지원 |
크로스브라우저에서는 쓸 수 없음 |
feImage가 체인에 있으면 HTML 요소에 필터 적용 실패 보고 |
SVG 콘텐츠에만 적용하거나 폴백 |
| 대형 요소에 필터를 걸면 고해상도에서 아예 렌더되지 않는 경우 보고 | 필터 적용 면적 제한 |
SVG <filter>보다 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
애니메이션되는 필터는 전정계 장애가 있는 사용자에게 실제로 불편을 준다. 모든 애니메이션 필터에 감소 모션 처리를 넣어라.
/* 방법 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로 처리해야 한다.
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를
<img>나 CSSbackground-image로 넣으면 브라우저가 샌드박스 처리해 내부의prefers-reduced-motion미디어쿼리가 평가되지 않는다. 인라인 SVG를 쓰거나, 애니메이션을 바깥 CSS에서 제어하라.
7.2 텍스트 가독성
- 왜곡 필터를 텍스트에 걸면 판독성이 떨어진다.
feDisplacementMap scale은 텍스트에서 4~8을 넘기지 마라. - 필터는 DOM 텍스트를 그대로 유지한다 — 선택·복사·검색·스크린리더 접근은 살아있다. 이것이 이미지로 굽는 것보다 나은 결정적 이유다.
- 듀오톤/포스터화로 대비가 낮아질 수 있다. 텍스트 위에 적용한다면 WCAG 대비비를 실제 렌더 결과로 다시 측정하라.
- 색수차(RGB split)는 난독증·시각 피로가 있는 사용자에게 특히 읽기 어렵다. 본문 텍스트에는 쓰지 마라.
7.3 필터 정의 SVG의 접근성
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<defs><filter id="...">...</filter></defs>
</svg>
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 저사양 기기 감지
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');
}
/* 무거운 필터를 값싼 대체물로 교체 */
.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 뷰포트 밖 / 백그라운드 탭에서 정지
애니메이션 필터에는 필수다.
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
- Performance 패널 → 녹화 → "Rendering" 트랙에서 Paint/Composite 시간 확인. 필터가 문제면 Paint가 길어진다.
- Rendering 탭 → "Paint flashing" 켜기 → 필터 영역이 매 프레임 초록색으로 깜빡이면 재계산 중이다.
- Rendering 탭 → "Layer borders"로 합성 레이어 승격 여부 확인.
- CPU throttling 4×/6× 로 저사양 기기 시뮬레이션. 이것이 가장 현실적인 테스트다.
9.2 A/B 비교 방법
필터 하나가 얼마나 비싼지 알고 싶으면 DevTools 콘솔에서 껐다 켜본다.
document.querySelectorAll('.effect').forEach(el => { el.style.filter = 'none'; });
FPS가 확 오르면 그 필터가 범인이다.
9.3 오프라인 도구
| 도구 | 용도 |
|---|---|
| yoksel/svg-filters | 필터 체인을 GUI로 만들고 코드 추출 |
| yoksel/svg-gradient-map | 듀오톤/트라이톤 tableValues 자동 생성 |
| fffuel 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를 쓰지 않았는가feTurbulenceseed가 알려진 문제 값이 아닌가- Safari / Firefox / 모바일 실기기에서 눈으로 확인했는가
접근성
- 애니메이션 필터에
prefers-reduced-motion처리가 있는가 - 필터 정의 SVG에
aria-hidden="true"가 있는가 - 텍스트에 필터를 걸었다면 판독성과 대비비를 확인했는가
- 필터가 전부 꺼져도 디자인이 성립하는가
레이아웃
- 필터를 건 요소 안에
position: fixed자손이 없는가 - 필터로 인한 스태킹 컨텍스트가
z-index계층을 깨지 않는가 - 필터 컨테이너 안에 본문 텍스트가 들어가 있지 않은가