HTML5 Canvas API를 활용하여 명언 카드를 그리는 컴포넌트에서, useEffect
를 통해 텍스트나 이미지가 업데이트된 이후에도 이전 내용의 잔상이 남는 문제가 발생하였다.
이 현상은 Canvas에 fillText()
, drawImage()
등을 사용할 때, 이전 그래픽 요소가 제거되지 않아 발생하는 것으로 확인되었다.
해당 컴포넌트는 다양한 onChange
이벤트에 반응하여 Canvas 상태를 업데이트하며, 이 과정에서 상태 간 의존성이 복잡하게 얽혀 있었다.
또한 상태 관리는 Zustand를 통해 전역적으로 처리되고 있었고, 하나의 useEffect
에서 여러 의존성이 얽혀 있는 구조로 인해 내부 동작을 예측하기 어려운 상황이었다.
⇒ 이로 인해 렌더링 주기와 Canvas 상태 반영 시점 간의 불일치가 발생하면서 문제로 이어졌다.
상태관리의 복잡성을 떠나 문제의 본질은 이전 그래픽 요소가 제대로 제거되지 않아 생긴 것으로, Canvas API의 clearRect()
메서드를 활용해 캔버스를 초기화하는 방식으로 해결을 시도하였다. 즉, 다음과 같은 과정을 통해 문제를 해결하였다.
초기화 함수 구현
clearRect(0, 0, canvas.width, canvas.height)
를 통해 Canvas 전체를 초기화하는 clearCanvas
함수를 정의하였다.
const clearCanvas = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
호출 위치 탐색
단순히 draw()
함수가 호출되기 직전에 초기화 함수를 넣는 것으로는 충분하지 않았다. draw()
는 여러 번 호출되며 이미지가 비동기적으로 로드되기 때문에, 잔상이 여전히 남는 문제가 있었다.
최적의 호출 타이밍 조정
최종적으로 imageEl
의 load
이벤트 내부에서 clearCanvas
를 실행하여 이미지가 완전히 로드된 후 캔버스를 초기화하고, 이후에 drawImage()
와 fillText()
가 실행되도록 로직을 변경하였다. 이로 인해 그래픽 요소의 렌더링 순서와 타이밍을 명확히 통제할 수 있었다.
[참고] 개선된 전체 코드
// Reset | 캔버스 초기화 함수 정의
const clearCanvas = ( ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement ) => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
// === 중략 ===
// 이미지 로드 이벤트 핸들
const loadImage = useCallback((textY: number, ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, split: string[]) => {
if (!imageEl) return
imageEl.alt = '명언 카드 배경 이미지'
imageEl.addEventListener('load', () => {
// Reset | 이미지가 로드된 후 캔버스 초기화 함수 호출
clearCanvas(ctx, canvas)
bgColorDraw(ctx, width, height)
ctx.fillStyle = `${color}`
ctx.drawImage(imageEl, 0, 0, canvas.width, canvas.height)
// 배열 형태로 분리된 텍스트를 조건에 따라서 다르게 렌더링한다.
split.forEach((text: string, i: number) => { // === 중략 === })
})
},[bgColorDraw, color, fontStyle, height, imageEl, lineHeight, width])
// 그리기
const draw = useCallback (
(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => {
// === 중략 ===
// Load Image | 이미지 로드 호출
loadImage(...)
}, [ bgImageSrc, ... ],
)
// 캔버스 생성
const createCanvas = () => { // === 중략 === }
useEffect(() => {
const { canvas, ctx } = createCanvas()
if (canvas && ctx) { draw(ctx, canvas) }
}, [draw])
useEffect
는 로직 흐름을 예측하기 어려워지고 디버깅이 힘들어질 수 있음을 체감하였다.useEffect
로 관리하기보다는, 책임을 분리하고 동작 순서를 제어하는 것이 중요함을 확인하였다.clearCanvas
호출 위치를 찾는 과정에서 렌더링 타이밍 제어의 중요성을 배웠고, 실제 적용을 통해 문제를 해결할 수 있었다.