개발새발 로그

[2024-01-25] 개인 프로젝트 회고 본문

React

[2024-01-25] 개인 프로젝트 회고

이즈흐 2024. 1. 25. 17:29

1. offsetX,offsetY와 getBoundingClientRect를 이용한 clientX,clientY

e.nativeEvent.offsetX 및 e.nativeEvent.offsetY를 사용하는 것과 getBoundingClientRect를 사용하는 것은 마우스 이벤트를 통해 마우스의 위치를 얻는 두 가지 다른 방법입니다.

e.nativeEvent.offsetX 및 e.nativeEvent.offsetY:

  • 이 방법은 이벤트가 발생한 엘리먼트 내에서의 상대적인 마우스 위치를 나타냅니다.
  • offsetX는 이벤트가 발생한 엘리먼트 내에서 마우스 위치의 x 좌표이며, offsetY는 y 좌표입니다.
const x = e.nativeEvent.offsetX; const y = e.nativeEvent.offsetY;
  • 이 방법은 이벤트가 발생한 엘리먼트 자체에 대한 상대적인 위치를 반환합니다.

getBoundingClientRect:

  • 이 방법은 이벤트가 발생한 엘리먼트를 기준으로 한 전체 문서(document)에서의 상대적인 마우스 위치를 계산합니다.
  • getBoundingClientRect를 사용하여 엘리먼트의 위치 및 크기 정보를 가져온 후, 해당 엘리먼트 내에서의 상대적인 마우스 위치를 계산합니다.
const container = containerRef.current; 
if (container) { 
	const rect = container.getBoundingClientRect();
    const x = e.clientX - rect.left; 
    const y = e.clientY - rect.top; 
}
  • 이 방법은 전체 문서를 기준으로 한 상대적인 위치를 반환합니다.

차이점:

  • 첫 번째 방법은 이벤트가 발생한 엘리먼트 내에서의 상대적인 위치를 제공하며, 해당 엘리먼트의 좌측 상단 모서리를 기준으로 합니다.
  • 두 번째 방법은 전체 문서를 기준으로 한 상대적인 위치를 제공하며, 엘리먼트의 좌측 상단 모서리를 기준으로 합니다.

 

어떤 방법을 선택할지는 상황에 따라 다를 수 있습니다.

만약 이벤트가 발생한 엘리먼트 내에서의 상대적인 위치만 필요하다면 첫 번째 방법을 사용하고,

전체 문서에서의 상대적인 위치가 필요하다면 두 번째 방법을 사용하면 됩니다.

 

2. requestAnimation이란?

https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C

 

🌐 웹 애니메이션 최적화 requestAnimationFrame 가이드

자바스크립트 웹 애니메이션 웹페이지의 애니메이션을 구현할때 CSS의 animatoin , transition , transform 속성을 통해 구현할 수도 있지만, 보다 사용자와의 복잡한 상호작용을 구현하게 하기 위해 Javasc

inpa.tistory.com

배경

나는 MouseMove 이벤트로 요소를 기준으로 좌표값을 가져와서
transform : rotateX rotateY로 마우스에 반응하여 기우는게 가능하도록 기능을 만들었다.

근데 이 기능을 만들고, 뭔가 버벅이는 느낌이 들었다.

 

예상하기로는 MouseMove 이벤트가 자주 일어나고, 요소는 이벤트에 따라 변하는 좌표에 맞춰서 기울어져야하기 때문에 
버벅이는 거라고 판단햇다.

그래서 방법을 찾던 도중 requestAnimation이라는 기능을 찾았다.

 

애니메이션 관련 최적화 API인 requestAnimationFrame을 자세히 알기 위해서 검색하면서 찾아보았고,

내가 기억하기 쉬운 방식으로 적어보려고한다.

 

1. 브라우저 렌더링 단계를 알아보자

1. 자바스크립트가 애니메이션 및 기타 작업 스크립트 수행 후 DOM생성

2. CSS규칙을 어떤 요소에 적용할지 계산하고 CSSOM 생성

3. 브라우저는 DOM과 CSSOM을 결합해 객체들의 위치와 크기들을 계산하는 렌더트리를 생성 - Layout단계

4. 브라우저는 렌더 트리를 사용하여 실제로 화면에 픽셀을 출력 - Paint 단계

5. 브라우저는 화면에 출력되는 객체들을 합성하여 최종 화면을 생성 - Composite단계

 

일단 이 과정을 자세히는 말고 위와같은 파이프라인 단계를 지나 화면에 그린다는 정도만 알아야한다고 한다.

 

2. 브라우저 프레임을 알아보자

우리가 아는 모니터에서 60hz, 120hz는 주사율을 뜻한다.

이는 1초동안 모니터 화면의 출력빈도를 나타내는 단위다.

그리고 보통 hz는 frame과 관련이 있다.

 

보통 인간의 눈은 1초에 60번 장면이 넘어가야 부드럽다고 느낀다고한다.

그래서 현대기기들은 60hz 혹은 60fps로 설계를 한다.

 

왜 이걸 알아야하냐면

애니메이션의 부드러운 움직임 효과를 주기 위해서는 

이 프레임 단위에 맞게 설계해야하기 때문이다.

 

초당 60개의 프레임을 렌더링 한다는 말은

16.666 밀리세컨드 ( 1000ms/ 60fps) 간격으로 프레임 생성이 필요한 셈이 된다.

 

따라서 자바스크립트로 사용자에게 부드러운 애니메이션을 구현하려면 16.6ms마다 코드를 호출하는 식으로 구현해야한다.

 

그럼 말 그대로 16.6ms마다 호출하도록 setTimeout과 setInterval로 해보자

const performAnimation = () => {
  /* 스타일 조정 스크립트 */
}

// 1초에 60번 무한 반복
setInterval(performAnimation, 1000 / 60)


...

const performAnimation = () => {
  /* 스타일 조정 스크립트 */
  
  setTimeout(performAnimation, 1000 / 60); // 함수 내부에서 다시 setTimeout을 호출하여 반복
}

setTimeout(performAnimation, 1000 / 60);

 

하지만 이 두 함수의 문제점은 주어진 시간내에 동작만 할 뿐 프레임을 신경쓰지 않는다.

프레임 단위로 프레임 시작 시간에 맞춰 실행됨을 보장하지 못하기 때문이다!

 

그니까 16ms 간격으로 프레임 단위가 진행되어야하는데

브라우저에서 다른작업으로 인해 지연되어 자바스크립트의 콜백함수 부분이 단위의 중간에서 호출되면

리플로우가 일어나서 

브라우저 렌더링 단계 중 Layout - Paint - Composite이 다시 일어나게 된다.

그러면 프레임이 생성되어야하는 시기에 프레임이 누락되서 1프레임 깎이게 된다.

쉽게말해서 스타일하는 함수가 호출되어야하는 시기에 호출을 못하는 것이다.

자바스크립트의 콜스택은 싱글스레드니까 하나의 하나만 수행하게 되는 것이다.

 

결과적으로 프레임 드랍이 일어나 버벅이게 된다.

이러한 지연발생에 대안으로 탄생한게 rAF(requestAnimationFrame)이다.

 

requestAnimationFrame

이 함수는 시스템이 프레임을 그릴 준비가 되면 애니메이션 프레임을 호출하여 애니메이션 웹페이지를 보다 원횔히거 혀울적으로 생성할 수 있도록 해준다.

실제 화면이 갱신되어 표시되는 주기에 따라 함수를 호출해주기 때문에 자바스크립트가 프레임 시작 시 실행되도록 보장해주어 위와 같은 지연현상을 방지해준다.

프레임 주기에 맞추어 일정한 간격으로 함수가 실행됨

requestAnimation을 통해 애니메이션을 구현할 때 

각 프레임이 브라우저의 프레임 주기에 맞추어 일정한 시간 간격으로 렌더링됨을 보여준다.

requestAnimationFrame 함수가 실행되면 브라우저는 다음 프레임이 그려지기 전에 함수를 실행하도록 예약하기 때문에 각 프레임이 정확히 16.6ms간격으로 렌더링 되게 된다.

 

그리고 애니메이션 프레임을 Paint할 준비가 됐을 때 호출한다는 부분이 

이전 setInterval방식처럼 Paint할 준비가 되지않아도 Paint를 요청하는것과 대비된다.

즉 웹브라우저에 최적화 되어있다.

또한 지연 및 블로킹 현상이 생기지않아 부드러운 애니메이션이 제공된다.

 

requestAnimationFrame 장점

1. 백그라운드 동작 정지

setInterval과 같은경우 계속 타이머가 돌기 때문에 시스템 리소스를 낭비하고 불필요한 전력을 소모한다.

하지만 rAF는 페이지가 비호라성화된 상태이면 페이지 화면 그리기 작업도 브라우저에 의해 일시 중지됨으로 리소스나 배터리 수명을 낭비하지 않게 된다.

 

2. 디스플레이 주사율에 맞게 호출

이 특징은 최적화된 웹 애니메이션을 구현하는데 있어 setInterval과 결정저인 차이점이라고 할 수 있다

만약 모니터가 144hz라면 웹브라우저는 해당 주사율에 맞춰서 rAF역시 144번 호출하게 된다.

 

3. Animation Frame 큐에서 처리

rAF도 setTimeout이나 여타 이벤트 핸들러와 같이 "애니메이션프레임"을 그리기 위한 콜백함수를 등록하고 비동기 task로 분류하여 처리된다.

이때 중요한 특징은 rAF는 일반 task queue가 아니라 animation frame이라는 별개의 queue에서 처리된다는 점이다.

 

rAF는 브라우저의  렌더링 엔진이 다음 프레임을 그리기 전에 실행해야 하는 rAF에 등록한 콜백 함수들을 담는 큐이다.

별도의 큐에서 적재되어 이벤트 루프에 의해 꺼내지기 때문에 실행이 뒤쳐지거나 하는 현상이 감소한다.

 

rAF 실제 사용 예제

 
 ...
 
 const animationFrameIdRef = useRef(0)

  const handleMouseMove = (e: React.MouseEvent<HTMLElement>) => {
    if (animationFrameIdRef.current) {
      cancelAnimationFrame(animationFrameIdRef.current)
    }

    animationFrameIdRef.current = requestAnimationFrame(() => {
      const x = e.nativeEvent.offsetX
      const y = e.nativeEvent.offsetY

      setOverlayX(x)
      setOverlayY(y)

      const tiltX = (4 / 30) * y - 20
      const tiltY = (-1 / 5) * x + 20

      setTiltX(tiltX)
      setTiltY(tiltY)
    })
  }

  const handleMouseLeave = useCallback(() => {
    if (animationFrameIdRef.current) {
      cancelAnimationFrame(animationFrameIdRef.current)
    }
    setTiltX(0)
    setTiltY(0)
    setOverlayX(0)
    setOverlayY(0)
  }, [])

  ...
}

 

 실제로 리액트에서 사용한 코드이다.

mouseMove를 할 때 계속해서 마우스의 좌표를 저장하고 

해당 좌표들로 이후 요소의 style의 transform : rotateX rotateY의 값을 조정한다.

 

그리고 MouseLeave, 마우스가 해당 요소에서 벗어나면 애니메이션 프레임을 제거한다.

 

애니메이션 타이머 이용하기 - 예제

let startTime;

// requestAnimationFrame의 콜백 함수에 매개변수를 정의
const draw = (timestamp) => {
    if (!start) start = timestamp; // 애니메이션 시작 시간

	currentTime = timestamp - startTime;

	// 애니메이션이 실행된지 2초가 지나면..
	if(currentTime > 2000) { 
        console.log('END');
        return;
    }

	requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

rAF에서 해당 애니메이션이 시작되면 그 시간을 알 수 있는 매개변수도 얻을 수 있다고 한다.

 

 

너무 다 중요한 내용이라 그대로 적은 것 같다.

전혀 알지 못했던 내용이고 

이걸 통해서 transform으로 효과를 주던 요소의 버벅임을 없앨 수 있었다.

 

 

너무 궁금해서 내 코드를 확인해보았다.

 const handleMouseMove = (e: React.MouseEvent<HTMLLIElement>) => {
    const x = e.nativeEvent.offsetX
    const y = e.nativeEvent.offsetY
    setOverlayX(x)
    setOverlayY(y)

    const tiltX = (4 / 30) * y - 20
    const tiltY = (-1 / 5) * x + 20

    setTiltX(tiltX)
    setTiltY(tiltY)
  }

MouseMove 이벤트를 할 때마다 좌표를 state에 저장하고 그 값을 transform rotateX rotateY에 넣는 중이다.

performance로 측정한 모습이다.

실제로 어떨 때는 버벅이고 

어떨 때는 버벅이지 않는 상황이다.

 

 

그럼 rAF를 적용하고나서 다시 측정해보자

뭔가 전체적으로 정돈되어있다.

requestAnimationFrame 는 Web API 다. 
이 메서드에 콜백으로 들어가는 메서드는 브라우저가 다음에 렌더링을 하기 전에 실행이되는걸 보장된다.

그럼 자세히 한번 봐보자

위 이미지처럼 AnimationFrame Fired가 추가되었다.그러면서 아래 function Call 이 생겼다추측건대 저 function Call에서 내가 작성한 좌표값을 할당하는 함수가 있는 것 같았다.그래서 저 함수가 repaint 전에 수행함을 보장함으로써 더 부드럽게 수행하는 것 같았다아직 추측이라서 이 문제는 추후 더 알아보고 수정해야겠다..

브라우저에서는 애니메이션을 그리기 위해 더 적합한 API인 requestAnimationFrame(이하 rAF)를 제공한다. rAF는 
브라우저에 의해 repaint 직전에 실행돼, 60 프레임 또는 모니터의 가변 주사율에 최대한 맞추어 콜백 함수를 호출

https://seukjjang.tistory.com/12

☝이 부분에서 중요하게 넘어가야할 점

1. rAF를 사용하지 않았을 때, 버벅이지 않다가 왜 개발자 도구를 열면 버벅이는 현상이 생길까?

2. will-change를 사용하는 것이 좋을까? 적용한다면 어느 부분이 좋을까?

 - rgb그림자 애니메이션이 무한으로 재생되는 부분

 - 마우스를 올렸을 때 move이벤트로 transform이 되는 부분

 그리고 만약 적용할때 어떻게 적용하는 것이 좋을까? - 아래 링크 참고

https://dev.opera.com/articles/ko/css-will-change-property/

3. rAF를 적용하면 개발자도구를 열어도 버벅이는 현상이 생기지 않는다 
버벅이는 현상이 왜 일어나지않는지 어떻게 알 수 있을까?

(Performance탭에서는 rAF를 사용한 것과 사용하지 않는 것을 측정했을 때 차이가 전혀 없는 것으로 보임)

(그저 위에서 말했듯이 Animation Frame Fired가 추가된 것과 추가되지 않은 것의 차이)

4. 쓰로틀링을 적용해서 16ms마다 실행하도록 하는 것이 좋을까?

 - 적용했을 때 차이가 없었음 (rAF를 적용안한 상황에서 쓰로틀링 적용해도 개발자 도구를 열었을 때 똑같이 버벅임)

 - 16ms로 지정하면 만약 144hz일때는 어떻게 대응해야하는지?

5. Performance 탭에서 측정했을때 나타나는 경고들

새로고침을 한 이후 4개의 지점에서 경고가 뜬다

경고는 아래와 같다.

Long task
강제 리플로우
Long Task

 

😂결론

결국 중요한 점은 rAF를 사용하는 것이 옳은지 옳지 않은지 아직 판단이 안된다.

버벅이는 현상이 개발자도구를 열었을 때와 같이 특정한 상황에만 나타나고,

rAF를 사용했을 때 눈에 띄게 버벅이는 현상이 없어졌지만 왜 없어졌는지를 모른다.

모른다기보다 그 증거가 보이지를 않는다..ㅠㅠ

 

 

 

3. Storybook CI/CD가 왜 안되는거지?

CI/CD가 잘되고 있다고 생각하고 프로젝트를 진행하고있었는데

깃허브 액션에서 PR이 올라가도 자동배포가 정상적으로 안되고 있었다..

그래서 왜그런지 찾아보니 문제가 많았다.

 

1. storybook.yml 누락

누락한 것은 내 잘못이라 어쩔 수 없었다.. 항상 코드는 잘 확인하자!

2. 블로그 코드를 복사한 문제

블로그에 나온대로 하면 되겠지? 했는데 아니었다.

https://velog.io/@doeunnkimm_/Storybook-%ED%8E%B8%ED%95%98%EA%B2%8C-%EC%97%B4%EC%96%B4%EB%B3%B4%EA%B8%B0-%EB%B0%B0%ED%8F%AC%EC%99%80-CICD-%EC%97%B0%EA%B2%B0

해당 블로그를 보고 따라했는데
처음 부분은 따라서 하면 되는 것은 맞았는데

storybook.yml 구성하는 부분이 npm이 아니고, yarn이었다...!

그래서 자동 배포에 계속 실패하고 있었다.

 

해결하기 위해서 npm으로 하신 분들걸로 계속 찾았다.

(사실 아직 정확하게 yml 파일안에서 무슨일이 일어나는지는 잘모른다..)

name: 'Chromatic Deployment'

on:
  pull_request:
    branches: [main]
    paths:
      - '**.stories.tsx'

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: cache dependencies
        id: cache
        uses: actions/cache@v3
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-storybook

      - name: Install dependencies
        if: ${{ steps.cache.outputs.cache-hit }} != 'true'
        run: npm ci

      - name: Publish to Chromatic
        uses: chromaui/action@latest
        id: publish_chromatic
        with:
          # ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          token: ${{ secrets.GITHUB_TOKEN }}
          onlyChanged: true
          exitZeroOnChanges: true
          autoAcceptChanges: true

      - name: comment PR
        uses: thollander/actions-comment-pull-request@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          message: '🚀storybook: ${{ steps.publish_chromatic.outputs.storybookUrl }}'

해당 코드가 나에게 잘 맞는 코드였다.

나중에 코드 한줄 한줄 해석을 해야되겠다고 생각했다..

 

3. 인증키 재 수정

나는 분명히 git hub의 Action에 secrets 토큰 값을 넣어줬었다.

근데 자꾸 failed to authenticate 라고 떴었고,

그래서 다시 토큰 값을 수정해주었다.

그랬더니... 자동배포가 정상적으로 작동했다.

아니.. 왜... 분명히 잘 넣어줬었는데 빈값이었나보다..아니면 토큰값에 오타가 있었다던지..ㅠㅠ

 

 

 

4. 깃허브 액션 nodejs16버전 중지(아직 해결 못함..)

아래와 같이 Annotaions가 떴다.

그래서 검색해보니 이제 node16버전은 못쓴다고 20버전을 사용해야한다고 한다.

 

그래서 아래와 같이 nodejs20버전을 사용하도록 했다

name: 'Chromatic Deployment'

on:
  pull_request:
    branches:
      - main

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: 20

  ...

그래서 이 부분이 추가되면서 nodeJS가 사용되는 것은 확인했다.

하지만 아직 똑같은 Annotartions가 뜨고있었다.

actions/cache@v3, 
thollander/actions-comment-pull-request@v2.

이 두개가 문제라고 나타냈는데

thollander에 관련된 깃허브를 들어가니 아래와 같이 버전20에 대해서 이슈가 올라와있었고,

아래 PR에서도 20 버전에 대해서 올라온 상태였다

이게 해결되면 Annotations가 해결되지않을까..? 하는생각에 
일단 현 상태를 유지하기로 했다.

(버전업그레이드 방법을 못찾음..)

728x90
반응형
LIST