본문 바로가기

[리액트] requestAnimationFrame으로 custom hook 만들기

feat. 결론은 뻘짓이었습니다. 글을 읽는 게 어떤 분께는 시간낭비일 수 있습니다.

 

 

reactjs에서 css로 애니메이션을 거는 부분에서 늘 고민인 부분은 처음 랜더링이 시작되었을 땐 애니메이션이 의도한 대로 동작하지않고 무시되는 경우였다.

 

아마 데이터를 불러오고 처음 마운트 될 때의 느린 SPA의 특징 때문인거 같았다.

 

최근에 웹챗 배포 스크립트를 작성하면서 자식 iframe과 부모 간의 postMessage 통신으로 부모에서 해당 챗봇 버튼을 display할 수 있도록 하는 로직을 구현했던 적이 있는데 이것을 커스텀 훅으로 바꿔서 적용하면 어떨까!라는 생각이 들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var leaflochatTimer=null;
function leafloInitFunc () {
  leaflochatTimer=null;
 
  requestLeafloChatIframe = function (e) {
    if("leaflo-ready"===e.data.event) {
      leaflochatTimer&&cancelAnimationFrame(leaflochatTimer); // requestAnimationFrame cancel
    }
  }
 
  function sendLeafloMessage () { // requestAnimtaionFrame function
    p.contentWindow.postMessage({event:"leaflo-import"},"*");
 
    leaflochatTimer = window.requestAnimationFrame(sendLeafloMessage);
  }
 
  window.addEventListener
  ?    (window.removeEventListener("message",requestLeafloChatIframe,!1),
    window.addEventListener("message",requestLeafloChatIframe,!1),
  :    window.attachEvent
  &&(window.detachEvent("onmessage",requestLeafloChatIframe,!1),
     window.attachEvent("onmessage",requestLeafloChatIframe,!1),
  sendLeafloMessage(); // 첫 실행
}
 
leafloInitFunc();
 

[webchat-embed.min.js]

 

 

이전에는 setInterval()과 같이 message를 못받을 경우를 대비하여 매 초마다 postMessage를 보내주고 있었는데 이 setInterval이나 setTimeout과 같은 함수는 프레임 손실이나 불필요한 비용 소모 (매번 콜백함수를 호출하기 때문에) 등의 문제가 있다.

 

따라서 어떤 연산이 다른 연산에 영향 없이 최대한 빨리 호출하기 위해 사용되는 requestAnimationFrame이 view와 관련한 시각적 변화를 일으킬 때 사용되며 이와 관련한 성능 최적화는 아래 링크에서 잘 설명해주고 있었다.

 

그래서 Mount 후에 어떤 함수를 requestAnimatoinFrame으로 원하는 시점만큼 실행시키면 무조건 랜더링 후에 실행되는 애니메이션이 보장되지 않을까! 라는 생각에서 커스텀 훅을 구현하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useEffect, useState } from "react";
 
const useAnimation = (f, s, v) => { // f: 실행 함수, s: 멈추는 함수(return boolean), v: 초기값
  let rafId = void 0;
 
  const [done, setDone] = useState(false);
 
  useEffect(_ => {
    if (window.requestAnimationFrame) {
      const render = _ => {
        v = f(v);
 
        rafId = window.requestAnimationFrame(render); 
 
        if (s(v)) {
          setDone(true);
          cancelAnimationFrame(rafId);
        }
      }
 
      render();
    }
  }, [])
  
  return {
    done
  };
}
 
export default useAnimation;

[useAnimation.js]

 

 

하지만.. 이걸 시범적으로 진행중이던 개인 웹 사이트에 적용해보니 일반 css를 사용하는 것과 랜더링 횟수에서 차이가 있었다.

 

 

붉은 박스 친 부분에 opacity와 translateY를 적용해보았다!

(count 변수는 rendering 횟수를 세기 위함)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import './Intro.style.css';
import React, { useRef, memo } from 'react';
import { Row, Col } from 'reactstrap';
 
import intro from '../../../assets/images/intro.png';
 
import * as uuidv4 from "uuid/v4";
import useAnimation from '../../../helpers/useAnimation';
 
let count = 0;
 
const Intro = props => {
  const randomUser = uuidv4();
  const imgRef = useRef();
 
  const animationFunc = yPos => {
    let style = imgRef.current.style;
 
    style.transform = `translateY(${yPos}px)`;
    return --yPos; 
  }
 
  const stopFunc = yPos => {
    if (yPos === 0) {
      return true;
    }
    return false;
  }
 
  const opacityFunc = opacity => {
    let style = imgRef.current.style;
 
    style.opacity = opacity;
    return opacity+0.1
  }
 
  const stopOpacityFunc = opacity => {
    if (opacity >= 1) {
      return true;
    }
    return false;
  }
 
  const { done: doneTransform } = useAnimation(animationFunc, stopFunc, 35);
  const { done: doneOpacity } = useAnimation(opacityFunc, stopOpacityFunc, 0);
 
  console.log("render ", count);
  count++;
 
  return (
    <Row className="py-3 row-filled intro" noGutters>
      <Col sm="6" className="title">
        <img src={intro} className="img-intro-title" ref={ imgRef } />
      </Col>
      <Col sm="6" className="d-flex-center chatbot">
        <ChatFrame user={ randomUser } />
      </Col>
    </Row>
  )
}
 
export default Intro;
 

requestAnimationFrame Hook 적용 랜더링 횟수

 

이 경우말고 애니메이션을 거는 흔한 방법 중 하나로 css에서 @keyframe과 animation 속성을 이용하는 것이었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import './Intro.style.css';
import React, { useRef, memo } from 'react';
import { Row, Col } from 'reactstrap';
 
import intro from '../../../assets/images/intro.png';
 
import * as uuidv4 from "uuid/v4";
import useAnimation from '../../../helpers/useAnimation';
 
let count = 0;
 
const Intro = props => {
  const randomUser = uuidv4();
 
  console.log("render ", count);
  count++;
 
  return (
    <Row className="py-3 row-filled intro" noGutters>
      <Col sm="6" className="title">
        <img src={intro} className="img-intro-title animated fadeUp" ref={ imgRef } />
      </Col>
      <Col sm="6" className="d-flex-center chatbot">
        <ChatFrame user={ randomUser } />
      </Col>
    </Row>
  )
}
 
/**
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
 
.fadeIn {
  animation-name: fadeIn;
}
 
.animated {
  animation-duration: 1s;
}
 
*/
 
export default Intro;
 

css로 animation 처리했을 때

뻘짓한건가...ㅠ

 

괜한 짓 했나 하고 잠시 후회했지만 이건 데이터 fetch가 없는 단순한 경우니까 한 번 본 프로젝트에 사이드 바가 스르륵 하고 나오는 부분이라던가 좀 더 복잡한 경우에 활용해보고 어떤지 결론지어야겠다! (아님 내가 잘못만들었거나 ㅠ)