프로덕션 WebGPU 엔진 구축... 정신치료 실무를 위해?

발행: (2025년 12월 24일 오전 06:36 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

도전 과제: 노이즈가 흐름이 되는 순간

Therapy Warsaw의 디지털 존재감을 구축하면서 우리는 특이한 요구사항에 직면했습니다. 우리는 스톡 사진이나 정적인 일러스트를 원하지 않았습니다. 살아있는 느낌을 주는, 항상 변화하지만 주의를 끌지 않는 생성형 텍스처를 원했습니다.

시각적 은유는 간단했습니다: 복잡한 패턴이 명료함을 찾아가는 과정. 잡음의 필드가 서서히 스스로를 조직화하여 일관된 흐르는 선이 되는 모습.

기술 요구 사항

  • 유기적이고 밀도 높음: 약 10,000개의 상호작용 입자.
  • 성능 중요: 사용자가 스크롤할 때 모바일에서 60 FPS 유지.
  • 탄력성: 10년 된 노트북(WebGL2)과 최신 장치(WebGPU) 모두에서 작동해야 함.
  • 프레임워크 비사용: React도, Three.js도 사용하지 않음. 오직 제어된, 유동적인 로직만 사용.

아키텍처: 듀얼‑스택 WebGPU + WebGL2 엔진

메인 스레드 외 렌더링

웹에서 무거운 그래픽을 다룰 때 가장 중요한 규칙: 메인 스레드에서 벗어나라.

스레드역할
메인 스레드DOM, 접근성, 라우팅, UI 상태
워커 스레드물리 연산, 기하 생성, OffscreenCanvas를 통한 렌더링

물리 시뮬레이션에 지연이 발생하더라도 페이지 스크롤은 부드럽게 유지됩니다. 전용 메시징 시스템을 통해 시각적 “프리셋”(색상, 속도, 난류)을 동기화하며, 블로킹 없이 통신이 이루어집니다.

// main.js
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
const offscreen = canvas.transferControlToOffscreen();

// Hand ownership to the worker
worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]);

WebGPU 구현

우리는 WebGPU로 시작했는데, Compute Shaders가 파티클 시스템에 자연스럽게 맞기 때문입니다.

Compute Passes

PassPurpose
Map Pass노이즈 텍스처(불꽃, 밀도, 공허 맵)를 생성
Flow Pass벡터 필드를 계산
Life Pass파티클 수명을 업데이트하고 리셋을 처리
Physics Pass흐름 벡터에 따라 파티클을 이동

핵심 성능 향상: CPU‑GPU 왕복을 피하는 것. 전체 시뮬레이션이 GPU에 머무릅니다.

Source:

WebGL2 Fallback Using Transform Feedback

WebGPU 지원이 점점 늘어나고 있지만 아직 보편적이지 않기 때문에 “멍청한” 대체 방안이 되지 않는 fallback이 필요했습니다.

  • Transform Feedback은 WebGL2가 정점 셰이더에서 입자 위치를 업데이트하고 이를 버퍼에 다시 기록하도록 하여, 컴퓨트 셰이더를 흉내냅니다.
  • 이 접근 방식은 CPU에 과도한 부담을 주지 않으면서 기능 동등성을 유지합니다.

부드러운 파라미터 전환: 임계 감쇠 스프링 시스템

사용자가 페이지를 이동할 때 시각화가 변형됩니다(색상이 변하고, 혼돈이 바뀌며, 속도가 조정됩니다). 단순한 선형 보간은 로봇처럼 보였기 때문에, 우리는 임계 감쇠 스프링 시스템을 구현했습니다:

function updateSpring(state, target, dt) {
    const tension = 120;
    const friction = 20;

    const displacement = target - state.value;
    const force = tension * displacement - friction * state.velocity;

    state.velocity += force * dt;
    state.value += state.velocity * dt;
}

매 프레임마다 약 20개의 스프링 구동 파라미터를 업데이트하고 이를 **Uniform Buffer Object (UBO)**에 업로드하여, 계산된 느낌이 아닌 물리적인 느낌의 전환을 구현합니다.

효율적인 트레일 렌더링

두꺼운 선을 렌더링하는 전통적인 방법은 세그먼트당 두 개의 삼각형(여섯 개 정점)을 생성하는 것이며, 긴 트레일에서는 비용이 많이 듭니다.

우리의 접근 방식

  • 각 선의 헤드 위치만 저장합니다.
  • 버텍스 셰이더 내부에서 루프(~60회 반복)를 실행하여 흐름 필드를 통해 경로를 역추적하고, 트레일을 실시간으로 재구성합니다.

장점: 대규모 대역폭 감소(선당 1점, 수천 개 정점이 아님).
단점: 정점당 ALU 비용 증가.

최신 GPU에서는 ALU가 저렴하고 대역폭이 비쌉니다. 이 트레이드오프를 통해 모바일 기기에서도 수천 개의 길고 부드러운 트레일을 렌더링할 수 있습니다.

결과

최종 사이트인 therapywarsaw.com은 살아있는 배경을 특징으로 합니다—작업의 특성을 반영하는 조용한 텍스처이며, 다양한 기기에서 성능을 유지합니다.

오픈 소스

엔진은 오픈 소스입니다:

github.com/23x2/generative-flow-field

셰이더 파이프라인이나 Transform Feedback 구현을 자유롭게 탐색해 보세요.

Back to Blog

관련 글

더 보기 »

시간이 변수로 변했을 때 — Numba와 함께한 나의 여정 메모 ⚡

배경 처음에는 성능을 추구하지 않았습니다. 저는 이미지 처리, 원격 탐사, NumPy 중심 워크플로와 같은 무거운 계산 작업에 깊이 몰두하고 있었고, 일은 점점 복잡해졌습니다. ### 문제 인식 - **느린 실행 시간**: 단일 이미지 처리 파이프라인이 수십 초에서 수분까지 걸렸습니다. - **메모리 사용량**: 대규모 위성 이미지(수천 메가픽셀)를 메모리에 올리면 시스템이 금방 메모리 부족 상태가 되었습니다. - **확장성 부족**: 현재 코드는 단일 코어에 최적화돼 있어 멀티코어·GPU 활용이 거의 불가능했습니다. ### 초기 접근법 1. **프로파일링**: `cProfile`, `line_profiler` 등을 사용해 병목 구역을 식별했습니다. 2. **벡터화**: 루프 기반 연산을 NumPy 배열 연산으로 교체해 평균 2~3배 가속을 얻었습니다. 3. **메모리 매핑**: `numpy.memmap`을 도입해 전체 이미지를 한 번에 로드하지 않고 슬라이스 단위로 접근했습니다. ### 한계와 교훈 - **벡터화만으로는 충분치 않음**: 일부 알고리즘(예: 복잡한 필터링, 비선형 변환)은 여전히 O(N²) 복잡도를 유지했습니다. - **Python GIL**: 멀티스레딩을 시도했지만, GIL 때문에 CPU 바운드 작업에서 기대한 속도 향상이 없었습니다. - **디버깅 난이도**: 메모리 매핑과 대형 배열을 다루다 보니, 인덱스 오류와 메모리 누수가 빈번히 발생했습니다. ### 전환점: Cython & Numba 도입 1. **Cython**: 핵심 루프를 Cython으로 포팅하고 `cdef` 타입 선언을 추가해 C 수준의 성능을 달성했습니다. 2. **Numba**: JIT 컴파일러인 Numba를 사용해 GPU 가속을 시도했으며, `@njit(parallel=True)` 데코레이터로 멀티코어 병렬화를 구현했습니다. 3. **프로파일링 재실시**: `nvprof`와 `perf`를 활용해 GPU 메모리 전송과 커널 실행 시간을 최적화했습니다. ### 결과 - **전체 파이프라인 실행 시간**: 평균 45초 → 3.2초 (≈14배 가속) - **메모리 사용량**: 32 GB → 8 GB (메모리 매핑 + 청크 처리) - **확장성**: 동일 코드베이스로 다중 GPU 클러스터에 배포 가능해졌으며, 작업당 비용이 70% 절감되었습니다. ### 주요 교훈 요약 - **프로파일링이 최우선**: 실제 병목을 정확히 파악해야 불필요한 최적화에 시간을 낭비하지 않는다. - **벡터화와 메모리 매핑은 기본**: NumPy를 최대한 활용하고, 대용량 데이터는 메모리 매핑으로 슬라이스 처리한다. - **Cython/Numba는 선택이 아닌 필수**: 순수 Python만으로는 CPU 바운드·GPU 가속 작업을 충분히 최적화하기 어렵다. - **테스트와 검증**: 최적화 전후에 동일한 결과가 나오는지 자동화된 테스트 스위트를 구축해 회귀를 방지한다. ### 앞으로의 계획 - **Dask와 Ray 도입**: 클러스터 수준에서 작업을 자동으로 스케줄링하고, 데이터 파티셔닝을 효율화한다. - **ONNX 및 TensorRT**: 기존 필터링 파이프라인을 딥러닝 모델로 전환해 추론 속도를 추가로 끌어올린다. - **CI/CD 파이프라인**: 최적화된 코드와 벤치마크를 지속적으로 검증하는 자동화된 워크플로를 구축한다. 이러한 과정을 통해 단순히 “코드를 빠르게” 만드는 것이 아니라, **확장 가능하고 유지 보수 가능한 고성능 시스템**을 구축하는 것이 목표임을 다시 한 번 확인했습니다.