TDS 인터랙션 컴포넌트 / 2026.01 ~ 2026.04
KawaseBlur 구현 상세
TL;DR
ProgressiveBlur를 실제 제품 컴포넌트로 만들 때는 Dual Kawase Blur를 WebGL 렌더 파이프라인으로 구성했어요.
알고리즘을 아는 것만으로는 부족했어요. 이미지와 비디오 입력을 안정적으로 렌더링할 수 있는 구조로 옮겨야 했어요. 원본 에셋을 texture로 올리고 downsample/upsample framebuffer를 거쳐 여러 blur level을 만든 다음, 최종 composite shader에서 픽셀 위치에 맞는 blur level을 가중 합성했어요.
React 컴포넌트 안에서 쓰이지만 실제 구현은 GPU 리소스의 생명주기를 직접 다루는 일에 가까웠어요. texture 업로드, framebuffer 구성, devicePixelRatio 보정, 비디오 프레임 갱신, WebGL context 제약까지 함께 고려해야 했어요.
문제 정의 Problem
앞선 단계에서 Dual Kawase Blur가 Figma의 ProgressiveBlur와 가장 가까운 결과를 낸다는 걸 확인했어요. 이제 필요한 건 이 알고리즘을 interaction 컴포넌트 안에서 안정적으로 렌더링할 구조였어요.
WebGL 구현은 shader 하나로 끝나지 않았어요. 원본 이미지나 비디오를 texture로 올려야 했고, downsample/upsample을 위한 framebuffer를 관리해야 했고, 마지막에는 위치별 blur amount에 맞게 여러 texture를 합성해야 했어요.
canvas는 CSS 픽셀과 실제 물리 픽셀이 다를 수 있어서 Retina 같은 고해상도 디스플레이에서는 별도 보정도 필요했어요. devicePixelRatio를 반영하지 않으면 원본 해상도로는 충분해 보여도 실제 화면에서는 이미지가 뭉개졌어요.
접근 방식 Approach
렌더링 과정은 단계별 파이프라인으로 나눴어요. source upload, downsample, upsample, composite 순서로 ProgressiveBlur 렌더 파이프라인을 구성했어요.
먼저 원본 이미지나 비디오는 originalTexture에 업로드했어요. downsample 단계에서는 /2, /4, /8, /16, /32, /64처럼 점점 작은 texture를 만들었고, upsample 단계에서는 각 blur level을 다시 원본 해상도 texture로 복원했어요. 그렇게 원본을 포함한 여러 장의 full-size blur texture를 만들면, composite 단계에서 이 texture들을 입력으로 받아 최종 이미지를 그렸어요.
최종 shader에서는 현재 픽셀 위치를 startPoint → endPoint 벡터에 투영해 blur amount를 계산했어요. 계산한 blur amount를 blur level로 변환하고, 인접한 blur texture들을 가우시안 가중치로 섞었어요. 이렇게 해야 level 사이가 끊겨 보이지 않고 연속적인 ProgressiveBlur처럼 보였어요.
정적 이미지와 비디오는 같은 renderer 구조에서 다루되, 비디오는 프레임 갱신 타이밍에 맞춰 texture를 다시 업로드하도록 했어요. 고해상도 디스플레이에서는 canvas 물리 픽셀과 CSS 픽셀의 차이를 맞추기 위해 devicePixelRatio를 보정했어요.
핵심 결정 Decision
픽셀마다 큰 blur radius를 직접 샘플링하지 않고, blur level을 미리 만들어둔 뒤 composite 단계에서 섞었어요.
직접 샘플링 방식은 blur radius가 커질수록 샘플 수가 늘고 비용도 빠르게 커졌어요. 반면 downsample/upsample으로 여러 blur level을 만들어두면 렌더링 비용을 더 예측 가능하게 잡고, Figma에 가까운 점진 blur도 만들 수 있었어요.
대신 texture와 framebuffer를 더 많이 써야 했어요. GPU 리소스의 생성과 해제도 직접 관리해야 했어요. 그래도 ProgressiveBlur의 정확한 시각 결과를 만들려면 이 복잡도를 감수할 필요가 있다고 봤어요.
제품 컴포넌트의 정확한 WebGL 결과와 에디터 환경의 안정성도 분리했어요. 하나의 Document 안에서 WebGL context를 무한히 만들 수는 없어요. 에디터처럼 같은 컴포넌트가 여러 위치에 반복해서 뜨는 환경에서는 인스턴스마다 WebGL context를 만드는 구조가 현실적으로 어려울 수 있었어요. 그래서 에디터에서는 CSS blur fallback을 함께 제공했어요.
해결 결과 Result
위치에 따라 blur 값이 달라지는 ProgressiveBlur를 WebGL canvas에서 렌더링했어요. 이미지와 비디오 입력을 같은 renderer 구조에서 다룰 수 있게 됐고, 단계별 blur texture를 쓰면서도 결과물은 연속적인 ProgressiveBlur처럼 보이게 만들었어요.
에디터에서는 WebGL context 제약을 고려해 CSS 버전으로 동작이 안정적으로 돌아가게 했어요. WebGL 구현만큼 정확한 ProgressiveBlur는 아니지만, 에디터 환경에서는 안정성을 먼저 챙길 수 있었어요.
배운 점 Learning
이 작업을 하면서 WebGL이 단순히 shader만 짜는 일이 아니라는 걸 배웠어요. texture 업로드, framebuffer 구성, canvas 물리 픽셀 보정, 비디오 프레임 갱신, GPU 리소스 정리까지 하나의 렌더 파이프라인으로 설계해야 했어요.
React 컴포넌트 안에서 구현하더라도 실제로는 GPU 리소스의 생명주기를 직접 관리해야 했고, canvas/WebGL 기반 시각 컴포넌트가 일반적인 DOM 컴포넌트와는 완전히 다르게 동작한다는 걸 체감했어요.
이 작업을 다시 한다면 Next
다음에 WebGL 기반 컴포넌트를 만든다면, 단일 화면에 몇 개의 인스턴스가 동시에 뜰 수 있는지부터 확인하려고 해요.
그 숫자를 모르면 인스턴스마다 context를 만드는 설계가 애초에 성립하는지도 판단하기 어려워요. 필요하다면 CSS fallback, context 공유, 렌더링 인스턴스 제한 같은 전략을 설계 초기에 함께 잡아야 한다고 배웠어요.