본문 바로가기
Rust

트러블 슈팅

by 쪼꼬에몽 2025. 11. 21.

특정 이미지에서 필터 스크롤 설정을 할 때 캔버스가 버벅이거나 끊기는 이유가 발생한다.

이유를 찾아보니 만든 에디터 구조에서 발생할 수 있는 문제였다.

 

원인 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