특정 이미지에서 필터 스크롤 설정을 할 때 캔버스가 버벅이거나 끊기는 이유가 발생한다.
이유를 찾아보니 만든 에디터 구조에서 발생할 수 있는 문제였다.
원인 1 - 초고해상도 이미지 + 큰 캔버스 사이즈
-> 고해상도 이미지를 그대로 캔버스 크기로 렌더링하면 비용이 많이 들 수 있다.
원인 2 - 스크롤 시 캔버스가 계속 Repaint됨
-> 캔버스는 React DOM과 다르게 GPU에 직접 렌더링 된다.
스크롤이 발생하면 브라우저는 Repaint 영역을 계산하여 캔버스를 GPU에서 다시 조합한다.
큰 캔버스일수록 시간이 오래 걸리게 된다.
원인 3 - 필터 적용 시 WASM를 매번 호출 -> CPU 부하
-> 나의 필터 로직은 slider value 변경 -> WASM에 다시 전달 -> 새 픽셀 계산 -> canvas.putImageData()
스크롤이 움직일 때마다
CPU: 필터 계산,
GPU: 캔버스 repaint,
JS main thread: 이벤트 처리,
layout + paint: 스크롤 재계산
이 들어가 부하가 발생한다.
크게 3가지 원인으로 분리했다.
1. 해상도 높은 사진 수정 시 필터 버퍼링 문제
해상도가 낮은 사진은 문제가 없다.
하지만 해상도가 높은 사진을 편집하려고 하면 필터 조절 시 버퍼링 문제가 발생한다.
이것을 해결하기 위해 먼저 이미지의 최대 해상도를 조절했다.
interface resizeImageProps {
width: number;
height: number;
}
export default function resizeImage({ width, height }: resizeImageProps) {
const MAX_SIZE = 1080;
if (width > MAX_SIZE || height > MAX_SIZE) {
const scale = MAX_SIZE / Math.max(width, height);
width = width * scale;
height = height * scale;
}
return { renderWidth: width, renderHeight: height };
}
해상도가 1080보다 높은 이미지인 경우에는 최대 해상도를 1080으로 설정하게 하였다.
이제 해상도가 매우 높은 이미지도 편집 시 버퍼링이 많이 줄어들었다.
2. 스크롤 시 캔버스가 계속 repaint 되는 문제
<div className="flex flex-row bg-black text-white h-screen gap-6 p-12">
<div className="flex flex-col p-4 border-2 min-w-[30%] h-full rounded-xl overflow-y-auto overlay-scroll">
<h2 className="text-2xl text-center font-bold m-4">편집 도구</h2>
<div className="flex flex-col gap-4">
{buttonFilters.map((filter) => (
<ButtonFilterComponent
key={filter.key}
disabled={disabled}
label={filter.label}
id={filter.id}
value={filters[filter.key]}
setValue={(v) => setFilter(filter.key, v)}
/>
))}
{sliderFilters.map((filter) => (
<SliderFilterComponent
key={filter.key}
disabled={disabled}
label={filter.label}
value={filters[filter.key]}
setValue={(v) => setFilter(filter.key, v)}
min={filter.min}
max={filter.max}
className='slider'
/>
))}
</div>
</div>
<UploadedImageComponent canvasRef={canvasRef} image={image} setImage={setImage} />
</div>
현재 페이지의 구조다.
필터 컴포넌트와 캔버스 컴포넌트로 분리되어 있다.
구조를 더 단순화시켜보면
<div className="flex flex-row h-screen p-12"> // 스크롤 컨테이너
<div className="overflow-y-auto"> // 패널만 스크롤
<UploadedImageComponent /> // 캔버스 영역
</div>
최상위 div 자체가 스크롤 영역이다.
div가 h-screen이지만, 브라우저 전체 구조상 window 스크롤이 발생하고 있다.
- 왼쪽 패널은 자체적으로 overflow 스크롤
- 하지만 전체 페이지(body)도 스크롤 가능한 상태
- 오른쪽 캔버스 영역은 body 스크롤의 영향을 받는다.
스크롤 발생 -> layout shift 계산
- 캔버스는 위치가 변하지 않아도 GPU 조합 다시 계산
- 캔버스 크기가 크면 GPU 부하 증가
- 스크롤 버벅임 발생
페이지를 구조화하는 작업을 할 것이다.
<div className="flex flex-row bg-black text-white h-screen overflow-hidden gap-6 p-12">
<FilterPanel
filters={filters}
setFilter={setFilter}
disabled={disabled}
/>
<CanvasPanel
canvasRef={canvasRef}
image={image}
setImage={setImage}
/>
</div>
FilterPanel과 CanvasPanel을 나누었다.
각 panel 안에는 필요한 컴포넌트가 들어있다.
EditorPage
- 전체 레이아웃 구조 분리
- overflow-hidden 적용 -> 페이지 전체 스크롤 제거
FilterPanel
- 버튼 필터와 슬라이더 필터 분리
- 패널 자체가 overflow-y-auto로 별도 스크롤
- 필터 UI 스크롤이 캔버스에 전혀 영향 없음
CanvasPanel
- 캔버스는 flex-1 독립 영역
- 스크롤 영향 없음
- 삭제/다운로드 UI는 오직 이 컴포넌트에서만 렌더링
- GPU Repaint의 걱정이 사라짐
이제 스크롤 할 때 canvas도 리렌더링되는 문제를 해결했다.
3. WASM 필터 과다 호출로 인한 CPU 부하
필터 슬라이더를 움직일 때 WASM이 매 프레임마다 호출된다.
슬라이더를 1만 움직여도 WASM이 계속 호출되므로 CPU 부하가 발생한다.
필터 조합 적용을 1번에 묶어서 실행되도록 할 것이다.
이러면 JS 메인 스레드가 가벼워지고,
전체 필터 적용이 GPU 수준으로 부드럽게 동작한다.
- 슬라이더 변경 디바운스 (필터 호출 빈도 낮춤)
- applyAllFilters 방식으로 필터를 한번에 처리 (CPU 부담 최소화)
- 원본 ImageData를 보존하고 누적 오차 제거 (화질 문제도 해결)
1) 슬라이더 입력 디바운스 적용
현재 구조
onChange -> setFilter -> useImageFilterController -> wasm.filter()
슬라이더 움직일 때마다 WASM 호출됨.
슬라이더 변경은 즉시 반영하되, WASM 적용은 1프레임 단위로만 실행되도록 함.
하지만,
디바운스를 찾아보던 중 이것을 적용하면 실시간으로 변화가 떨어질 것 같았다.
찾아보니
디바운스 단독 -> 사용자가 멈추면 갑자기 적용 -> 실시간성 떨어짐.
이라는 결과가 나올 수 있다고 했다.
- 사용자가 빠르게 값을 바꾸면 마지막 값만 전달
- 중간 값들은 모두 무시
이 프로젝트를 하면서
필터 슬라이더를 움직일 때 실시간으로 이미지가 매끄럽게 변하고 렉은 줄이고 싶었다.
발견한 방식이
쓰로틀 + 디바운스 하이브리드 방식 이다.
쓰로틀
- 16ms ~ 30ms 단위로 WASM 호출 (최대 60fps ~30fps)
- 슬라이더 움직일 때도 끊김 없이 반영됨
디바운스
- 슬라이더를 완전히 멈추면
- 50ms 후 최종 고하질 필터 1회 더 적용
UI 즉시 반영 + 실시간 변화 + 렉 감소 + CPU 절약
useRafThrottle.ts
- requestAnimationFrame 기반의 throttle 함수
import { useRef } from "react";
export default function useRafThrottle<T extends (...args: any[]) => any>(fn: T) {
const ticking = useRef(false);
return (...args: Parameters<T>) => {
if (ticking.current) return;
ticking.current = true;
requestAnimationFrame(() => {
fn(...args);
ticking.current = false;
});
};
}
- 슬라이더를 움직여도 WASM 필터 연산을 1초에 60번 이하로 강제 제한
이것을 통해 해결하고 싶은 점
- WASM 필터가 너무 무거우면 렉 걸림
- 사용자가 슬라이더를 드래그하는 동안 함수가 수백 번 실행됨
- WASM 처리가 너무 많아져서 프레임 드랍 발생함
const ticking = useRef(fasle);
- 지금 실행 중인지 아닌지를 기억함
- React 렌더링과 상관없이 영구 유지됨
- 연속 호출을 제한하는 핵심 상태
return (...args: Parameters<T>) => {
- 사용자가 호출할 쓰로틀된 함수를 만들어서 리턴함
- 원래 함수 fn 대신 이 함수를 실행하게 됨
if (ticking.current) return;
- 사용자가 슬라이더를 움직일 때
- applyPipelin() 같은 함수가 연속으로 계속 호출됨
- 현재 실행 중이면 새 호출은 무시함
requestAnimationFrame에 등록
ticking.current = true;
requestAnimationFrame(() => {
fn(...args);
ticking.current = false;
});
requestAnimationFrame 이란?
- 브라우저가 다음 화면 재렌더링 직전에 실행하는 콜백
- 1초에 최대 60fps
실행 흐름
- 첫 호출 -> 실행 예약함
- 그다음 반복 호출은 모두 무시됨
- 브라우저의 다음 frame에서 fn() 실행
- 끝나면 다시 ticking = false
- 다음 frame에서 1번씩 다시 실행 가능
사용자가 필터 슬라이더를 10ms마다 값 변경
-> WASM 필터는 16ms(60fps)마다 실행됨
실제 슬라이더 입력 → 60, 61, 62, 63, 64, ... (많음)
쓰로틀된 실행 → 60, 63, 68, 74 ... (1초 최대 60번)
디바운스도 넣어서 마지막 필터 적용하기
useEffect(() => {
if (disabled) return;
const id = setTimeout(() => {
applyAllFilters(filters);
}, 120);
return () => clearTimeout(id);
}, [filters]);
throttle
- 얼마나 많이 호출되든 상관없이
- 정해둔 주기보다 자주 실행되지만 않게 해줌
debounce
- 사용자가 입력을 멈춘 뒤
- 일정 시간이 지나면 딱 한 번 실행함
- 중간 입력이 계속 바뀌면 실행이 안되고 마지막 것만 실행
'Rust' 카테고리의 다른 글
| [트러블슈팅]next.js16과 tailwindcss v4 반응형 웹 오류 (3) | 2025.11.22 |
|---|---|
| 이미지 다운로드하기 (0) | 2025.11.20 |
| 대비 만들기 (0) | 2025.11.18 |
| 밝기 조절하기 (0) | 2025.11.18 |
| 파일 업로드 & 흑백 필터 적용 (0) | 2025.11.14 |