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