프로그래머스 데브코스 5기 TIL 79 - requestAnimationFrames, sockjs global객체 with vite

김영현·2024년 3월 11일
0

TIL

목록 보기
90/129

requestAnimationFrames

https://velog.io/@loevray/%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC-6-JS%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84#animation-frames

예전에 작성했던 글이 있다.
이번 프로젝트에서 다른 팀원분께서 사용하셨는데, 기억이 가물가물해서 조금 더 자세하게 정리해보려고 한다.

JS의 애니메이션

애니메이션눈속임이다.
쉽게 말해 영화와 같다. 일반적인 영화는 1초에 24장의 사진을 보여준다.
참고로 사람의 눈은 대략 초당 8장이면 영상으로 인식한다.(24프레임이라는 말도 있지만, 8프레임 애니메이션이 있는걸로 보면..ㅎㅎ)

아무튼, 애니메이션은 결국 초당 몇 장의 사진을 보여주느냐가 핵심이다.

JS에서도 이를 구현하기위한 방법은 보통 2가지가 있다. (CSS도 있지만, 여기선 JS의 애니메이션이니 다루지 않겠다!)
또한 최근 게이밍 모니터가 많이 보급되면서 144hz모니터가 많아졌지만, 그래도 평범한 60hz모니터 기준으로 설명하겠음.

  1. setInterval()을 이용.
  2. requestAnimationFrames를 이용.

setInterval()을 이용하는 경우부터 알아보자.

setInterval을 이용한 JS 애니메이션

코드 출처는 : https://tr.javascript.info/js-animation

초당 60프레임을 보여주려면, 약 16.6ms마다 변화를 주면 된다.

//시작한 시점의 시각을 저장.
let start = Date.now();

let timer = setInterval(function() {
  let timePassed = Date.now() - start;

  //2초가 지나면, 애니메이션이 끝난다.
  if (timePassed >= 2000) {
    clearInterval(timer); 
    return;
  }

  draw(timePassed);

 // 1000ms / 16.6ms = 초당 약 60회 만큼 작동하게 한다.
}, 16.6);

//timePassed는 0에서 2000까지올라간다. 따라서 왼쪽으로 0px부터 400px까지 움직인다. 
function draw(timePassed) {
  train.style.left = timePassed / 5 + 'px';
}

잘 작동할까? 잘 작동한다. 하지만 자바스크립트의 특징 + 브라우저 렌더링 과정으로 인해 헛점이 발생한다.
이 함수(draw1)를 호출하면 브라우저는 화면을 다시 그린다.(위치가 변경되므로 reflow 발생.) 이는 가벼운 연산이 아니다.
이 시점에 싱글 스레드인 자바스크립트는 다음 draw2함수가 호출되길 기다린다(reflow 동안..).
그러나, reflow16.6ms보다 길어졌고 그 다음draw2함수를 호출했지만, 프레임이 누락된 것 처럼 보인다.

이는 애니메이션이라는 방식에 전혀 부합하지 않는다. 또한 setInterval()을 이용한 애니메이션은 사용자가 탭을 보지 않아도 동작한다.

암만 들어도 최적화가 필요해보이는데, 뭐 없을까?

requestAnimationFrames

위의 문제를 해결하기위해 등장 한 것이 requestAnimationFrames()다.
문제는 reflow마다 애니메이션이 실행되어야하지만, 그렇지 못했다는 점!

재귀적으로 콜백을 받아 브라우저의 reflow와 동기화하여 콜백을 실행해준다.

function animate({timing, draw, duration}) {

  //여기서 사용된 perfomance.now는 페이지가 열린이후 부동 소수점 밀리초로 반환한다. Date.now등으로 사용하던 값을 대신하는 고해상도 측정값임.
  let start = performance.now();

  requestAnimationFrame(function animate(time) {
    // timeFraction goes from 0 to 1
    let timeFraction = (time - start) / duration;
    if (timeFraction > 1) timeFraction = 1;

    // calculate the current animation state
    let progress = timing(timeFraction)

    draw(progress); // draw it

    if (timeFraction < 1) {
      requestAnimationFrame(animate);
    }

  });
}

또한, requestAnimationFrames()는 화면에 reflow가 일어날때 호출되므로 사용자가 다른탭을 보고 있다면 애니메이션이 실행되지 않아 불필요한 사용자의 자원소모를 줄일 수 있다.


SockJs 에러 feat.Vite, 전역객체, cdn

Vite에서 SockJs-client(웹소켓 에뮬레이터)를 import로 사용하면 작동하지 않고 cdn(html파일에 링크로 삽입)으로 사용하면 작동하는 이슈가 있엇다.

처음에는 global is not defined라는 오류가 발생했다. 즉, 전역 객체가 정의되지 않았다는 것이다.
이상하다. JS에서는 전역 객체로 선언된 객체들이 항상 존재한다.

브라우저환경에선 window. Node.js환경에서는 global.
각자 대응되는 전역객체가 무조건 존재해야만 한다.
=> alert, setTimeout등은 모두 전역 객체의 프로퍼티기에 암묵적으로 windowglobal을 표기하지않고 사용할 수 있는것.

그런데 전역객체가 없다니?

global이 없는 이유



일단, Vite에는 노드에서 사용하는 global객체가 없다.(하지만, globalThis는 있음. 아마 브라우저의 window를 가져올듯.)


출처1 : https://github.com/vitejs/vite/issues/2778#issuecomment-810086159
출처2: https://github.com/vitejs/vite/discussions/5912

SockJs-clientglobal객체를 필요로했다.
하지만, 이는 노드 환경의 전역객체다. 따라서 라이브러리가 이에 의존해선 안된다.
라이브러리는 플랫폼에 중립적이어야한다. (알아서 polyfill이나 shim을 넣어두었어야 한다.)

예를들어 노드의 fs는 파일 시스템에 접근하는 기능이다. 어떤 라이브러리가 fs모듈에 의존한다면, 브라우저에서는 동작하지 않게 된다.
따라서 SockJs-clientglobal객체가 정의되지 않았을 때도 작동하게 만들어야 했다는 소리다.

Vite가 아니라 esbuild

사실, Viteglobal이 없는게 아니라 정확히는 Vite가 사용하는 번들러인 esbuild에 없는 것이다 ㅎㅎ


출처 : https://github.com/evanw/esbuild/issues/73

그러면, CDN으로 사용했을땐 왜 가능했을까?
=> CDN을 통해 삽입된 라이브러리는 브라우저 환경에서 구동되므로 전역객체를 참조할때window를 참조한 것이다.

해결법

처음에는 설정파일을 수정하여 해결하려고 했음.

//vite.config.ts

export default defineConfig({
  plugins: [react(), mkcert()],
  resolve: {
    alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }],
  },
   define: {
    global: {},
  },
});

이렇게 하면 된다는 말이 있어서 했지만, 웹소켓 연결은 에러조차 나지 않고 묵묵 부답이었다.

또한 이렇게 사용하는건 안티패턴이었다. 그 이유는....


공식문서에 이렇게 나와있다고 2022년에 답변하셨지만, 공식문서를 찾아보니까 이런말은 없긴 하다. 흠?
출처 : https://stackoverflow.com/questions/72114775/vite-global-is-not-defined

공식문서에 없다 해도 전역에 global을 셋팅해놓는건 예기치않은 사이드 이펙트를 발생시킬 수도 있어 보인다.
그리하여 찾은 해결법은...

//init.js
window.global ||= window;

위와같이 파일을 선언하고, 노드 전역객체인 global을 참조하는 라이브러리를 사용하는 파일 내 최상단에 import 해준다.

import "./init"

...
const client = new SockJs(...)

이러면 global객체를 참조할때 브라우저환경에서는 암묵적으로 전역객체의 하위 프로퍼티에서 찾게된다.
따라서 window.global을 찾는 거고, window.global에 실제 브라우저 전역객체인 window를 할당하여 브라우저 환경에서는 에러가 나지 않게 된다.

노드는 몰루?


느낀점

코어 지식은 중요하다. 이는 트러블 슈팅을 겪을때마다 매번 느낀다. 이번에는 컴퓨터 구조나 언어 관련 코어지식이 아닌, 환경에 관한 코어 지식이었다.

해결!

profile
모르는 것을 모른다고 하기

0개의 댓글