안녕하세요, 개발자 Sean입니다.
최근 캔버스를 이용해 어플을 제작하는 재미에 푹 빠져있는데요,
회사 프로젝트에도 한번 사용해 보려고 프리로딩 컴포넌트를 캔버스를 이용해 제작했습니다.
웹 어플 라이브러리로 리엑트를 사용하고 있기 때문에 useRef로 캔버스를 제어해 아래와 같이 멋진 다이내믹한 프리로딩 이펙트를 만들었습니다.
원이 돌아가며 작은 입자를 뿌리는 방식의 애니메이션 입니다.
하지만 오늘 이것저것 테스트 진행 도중 심각한 기능저하 문제를 발견했습니다.
프리로더가 언마운트 된 뒤에 어플의 속도가 급격하게 느려지는 문제가 발생했습니다 :(
우선 개발자 도구를 통해 퍼포먼스를 측정해 보았습니다.
역시나 메모리 누수가 발생하고 있는 것을 확인할 수 있었습니다.
어디가 원인인지 한참을 고민하다가
requestAnimationFrame함수가 언마운트 된 이후에도 계속 실행되고 있는 사실을 알게 되었습니다.
이 문제를 해결하는 방법은 간단합니다.
컴포넌트가 언마운트 될 때 (리엑트 훅에서는 useEffect의 리턴) cancleAnimationFrame 함수를 실행시키면 됩니다.
cancleAnimationFrame은 인자로 number를 받고 있는데요, requestAnimationFrame이 반환하는 아이디(넘버)를 넣어주면 해당 애니메이션을 종료하게 됩니다.
type Props = {
close: () => void
}
const Preloader = ({ close }: Props) => {
const ref = useRef<HTMLCanvasElement>(null);
const dpr = window.devicePixelRatio;
let canvasWidth = window.innerWidth;
let canvasHeight = window.innerHeight;
const interval = 1000 / 60;
const silverlines: any[] = [];
const particles: any[] = [];
const createSilverlines = () => {
const NUM = 20;
for (let i = 0; i < NUM; i++) {
silverlines.push(new SilverLine())
}
}
const createParticles = () => {
const NUM = 40;
for (let i = 0; i < NUM; i++) {
particles.push(new Particle())
}
}
const init = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => {
canvasWidth = window.innerWidth;
canvasHeight = window.innerHeight;
canvas.style.width = canvasWidth/2 + 'px';
canvas.style.height = canvasHeight/2 + 'px';
canvas.width = canvasWidth * dpr;
canvas.height = canvasHeight * dpr;
ctx.scale(dpr, dpr);
createSilverlines();
}
let stage = 0;
const requestRef = useRef<number>();
const render = (ctx: CanvasRenderingContext2D) => {
let now, delta;
let then = Date.now();
const frame = () => {
requestRef.current = requestAnimationFrame(frame);
now = Date.now();
delta = now - then;
if (delta < interval) return;
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
if (stage > 0 && stage < 6) createParticles();
for (let i=0; i < silverlines.length -1; i++) {
silverlines[i].update(stage)
silverlines[i].draw(ctx)
}
for (let i=0; i < particles.length -1; i++) {
particles[i].update(stage)
particles[i].draw(ctx)
}
for (let i = particles.length -1; i >=0; i--) {
if (particles[i].opacity <= 0) particles.splice(i, 1)
}
then = now - (delta % interval);
}
requestRef.current = requestAnimationFrame(frame);
}
useEffect(() => {
const canvas = ref.current;
const ctx = canvas?.getContext('2d');
if (!ctx || !canvas) return;
init(canvas, ctx);
render(ctx);
return () => {
if (requestRef.current) cancelAnimationFrame(requestRef.current);
};
},[])
const handleClose = () => {
stage = 8
setTimeout(() => {
close()
}, 1500)
}
return (
<Container onClick={updateStage}>
<Motion>
<canvas ref={ref}/>
</Motion>
<ButtonWrapper>
<Button type="primary" leftIcon={<NotAllowedIcon />} onClick={handleClose}>Stop</Button>
</ButtonWrapper>
</Container>
)
}
export default Preloader;