1. Transition πŸ‘©β€πŸ’»

λΈŒλΌμš°μ €κ°€ μ• λ‹ˆλ©”μ΄μ…˜μ„ ν‘œν˜„ν•  수 μžˆλŠ” 방법이 무엇이 μžˆμ„κΉŒ? κ°€μž₯ 기본적이고 μ‰¬μš΄ 것! λ°”λ‘œ CSS 의 transition을 μ‚¬μš©ν•˜λŠ” 것이닀. transition 은 μ„±λŠ₯도 μ’‹κ³ , μ‚¬μš©ν•˜κΈ° μ‰¬μ›Œ λ‹¨μˆœν•œ μ• λ‹ˆλ©”μ΄μ…˜ κ΅¬ν˜„μ— κ°€μž₯ 많이 μ‚¬μš©λ˜λŠ” 방법이닀. ν•˜μ§€λ§Œ μ—¬κΈ°μ—λŠ” ν•œ 가지 문제점이 μ‘΄μž¬ν•œλ‹€.

λ°”λ‘œ μ• λ‹ˆλ©”μ΄μ…˜ μ μš©μ„ μœ„ν•΄μ„œλŠ” hoverκ°€ λ°œμƒν•œλ‹€κ±°λ‚˜ classκ°€ λ³€κ²½λ”˜λ‹€κ±°λ‚˜ ν•˜λŠ” λ“± μ‚¬μš©μžμ˜ μΈν„°λž™μ…˜μ΄ λ°œμƒν•˜κ±°λ‚˜ JavaScript μ½”λ“œμ— μ˜ν•΄ class κ°€ λ³€κ²½λœλ‹€κ±°λ‚˜ ν•΄μ„œ μ μš©λ˜μ–΄μ•Ό ν•˜λŠ” CSS 의 변경이 λ°œμƒλ˜μ–΄μ•Όν•œλ‹€. 즉, μ• λ‹ˆλ©”μ΄μ…˜μ΄ μž‘λ™ν•˜κΈ° μœ„ν•΄μ„œλŠ” μ™ΈλΆ€ κ°œμž…μ΄ λ°˜λ“œμ‹œ ν•„μš”ν•˜λ‹€. 그리고 μ• λ‹ˆλ©”μ΄μ…˜μ„ λ°˜λ³΅ν•˜κ³  싢을 경우 setInterval 의 도움을 λ°›μ•„μ•Ό ν•˜κ³ , 두 개의 μ• λ‹ˆλ©”μ΄μ…˜μ„ μ΄μ–΄μ„œ μ—°μ†μœΌλ‘œ μž‘λ™μ‹œν‚€κ³  싢을 경우 μ—­μ‹œ setTimeout 의 도움을 λ°›μ•„μ•Όν•˜λŠ”λ°, JavaScript κ°€ μ‹±κΈ€μŠ€λ ˆλ“œμ΄λ‹€λ³΄λ‹ˆ μ •ν™•ν•œ μ• λ‹ˆλ©”μ΄μ…˜ κ΅¬ν˜„μ΄ λ˜μ§€ μ•Šμ„ 수 μžˆμ„ 뿐 μ•„λ‹ˆλΌ λ‹¨μˆœν•œ μ• λ‹ˆλ©”μ΄μ…˜ 연결을 μœ„ν•΄μ„œ CSS 도 JavaScript 도 λ„ˆλ¬΄ λ‚œμž‘ν•΄μ§€κ²Œλ˜λŠ” λ¬Έμ œκ°€ μžˆλ‹€.

λ”°λΌμ„œ μ‚¬μš©μž μΈν„°λž™μ…˜μ— λ°˜μ‘ν•˜λŠ” λ‹¨μˆœν•œ μ• λ‹ˆλ©”μ΄μ…˜ 효과λ₯Ό 쀄 λ•ŒλŠ” 맀우 νš¨μœ¨μ μ΄μ§€λ§Œ κ·Έ μ™Έ κ²½μš°μ— μ‚¬μš©ν•˜κΈ° νž˜λ“€λ‹€λŠ” 단점이 μžˆλ‹€.


2. setInterval πŸ‘©β€πŸ’»

1. Problems of the setInterval

κ·Έλ ‡λ‹€λ©΄ setInterval을 μ‚¬μš©ν•˜λŠ” 것은 μ–΄λ–¨κΉŒ? μ• λ‹ˆλ©”μ΄μ…˜ 효과λ₯Ό μ˜€λ‘œμ§€ JavaScript 만으둜 μž‘μ„±ν•˜κΈ° λ•Œλ¬Έμ— μ• λ‹ˆλ©”μ΄μ…˜ λ°˜λ³΅μ΄λ‚˜ μ—°κ²° λͺ¨λ‘ μ‚¬μš©μž μΈν„°λž™μ…˜ 없이 μ½”λ“œλ§ŒμœΌλ‘œ λͺ¨λ“  것을 μ œμ–΄ν•  수 μžˆλ‹€λŠ” μž₯점을 κ°–λŠ”λ‹€.

ν•˜μ§€λ§Œ setInterval 은 μΆ”μ²œλ˜λŠ” 방법이 μ•„λ‹ˆλ‹€. μ™œ 그럴까?

  1. ν”„λ ˆμž„ 손싀 λ°œμƒ κ°€λŠ₯성이 λ†’λ‹€.
  2. ν•˜λ“œμ›¨μ–΄ 가속을 μ‚¬μš©ν•˜μ§€ λͺ»ν•œλ‹€.

μš°μ„  첫 번째, κ°€μž₯ 큰 λ¬Έμ œκ°€ ν”„λ ˆμž„ 손싀이닀. JavaScript κ°€ μ‹±κΈ€μŠ€λ ˆλ“œμΈ 것과 관련이 μžˆλ‹€. HTML 및 CSS νŒŒμΌμ„ μ½μ–΄μ„œ νŒŒμ‹± ν•˜κ³  Object Model(DOM, CSSOM) 트리λ₯Ό 생성 ν›„ λ ˆμ΄μ•„μ›ƒμ„ 작고 μŠ€νƒ€μΌμ„ μ μš©ν•˜λ©΄ λ Œλ”λ§μ΄ μ™„λ£Œλœλ‹€. 그런데 μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ μš©λœλ‹€λŠ” 것은 λ Œλ”λ§ κ³Όμ • 쀑 λ ˆμ΄μ•„μ›ƒμ„ 작고 μŠ€νƒ€μΌμ„ μ μš©ν•˜λŠ” Reflow와 Repaintκ°€ μ§€μ†μ μœΌλ‘œ λ‹€μ‹œ ν˜ΈμΆœλ˜λŠ” μ‹€ν–‰λ˜λŠ” μƒνƒœλ₯Ό μ˜λ―Έν•œλ‹€.

2. Display Hertz

ν•˜λ“œμ›¨μ–΄ 가속을 μ§€μ›ν•˜μ§€ μ•Šμ•„μ„œ μ„±λŠ₯이 쒋지 λͺ»ν•˜λ‹€λŠ” 것은 μ‰½κ²Œ 이해할 수 μžˆλ‹€. ν•˜μ§€λ§Œ ν”„λ ˆμž„ 손싀이 λ°œμƒν•  κ°€λŠ₯성이 λ†’λ‹€λŠ” 것은 μ™œμΌκΉŒ?

λΈŒλΌμš°μ €λŠ” μ›λž˜ μ• λ‹ˆλ©”μ΄μ…˜ 같은 μž‘μ—…μ„ 염두해두고 λ§Œλ“€μ–΄μ§€μ§€ μ•Šμ•˜μ—ˆλ‹€. 정적인 데이터λ₯Ό 좜λ ₯ν•˜λ„λ‘ λ§Œλ“€μ–΄μ‘Œλ‹€λŠ” 것이닀. 그리고 JavaScript λ₯Ό κΈ°λ³Έ μ–Έμ–΄λ‘œ μ±„νƒν•˜λ©΄μ„œ λ©€ν‹°μŠ€λ ˆλ“œ ν™˜κ²½μ΄ λΆˆκ°€λŠ₯ν•΄μ‘Œλ‹€. 이런 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ Service Workerκ°€ λ„μž…λ˜μ—ˆμ§€λ§Œ λΈŒλΌμš°μ €μ˜ λ Œλ”λ§μ€ λ©”μΈμŠ€λ ˆλ“œμ—μ„œ λŒμ•„μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— 무거운 μž‘μ—…μ„ Worker λ₯Ό 톡해 돌릴 수 μžˆμ–΄λ„ λ©”μΈμŠ€λ ˆλ“œμ˜ interrupt λ₯Ό 막을 μˆ˜λŠ” μ—†λ‹€.

ν”„λ ˆμž„κ³Ό μ£Όμ‚¬μœ¨μ΄ μ–΄λ–€ 관계에 있길래 이것이 μ• λ‹ˆλ©”μ΄μ…˜μ— μ€‘μš”ν•œ 영ν–₯을 λ―ΈμΉ˜λŠ” 것인지 TV의 사둀λ₯Ό 보자. μ˜ν™”λŠ” μ˜›λ‚ λΆ€ν„° μ§€κΈˆκΉŒμ§€ λŒ€λΆ€λΆ„ 23.976fps λ₯Ό μ‚¬μš©ν•˜κ³  μžˆλ‹€. μ•½ 24fps λΌλŠ” 말이닀. 그러면 λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨μ΄ 24fps 의 μ •μˆ˜λ°°λ₯Ό κ°€μ Έμ•Ό ν”„λ ˆμž„ 손싀이 μ—†λ‹€.

그런데 TV 방솑 μ‹œμž₯이 λ””μ§€ν„Έλ‘œ λ„˜μ–΄μ˜€λ©΄μ„œ NTSC 방식을 μ‚¬μš©ν•˜λŠ” μš°λ¦¬λ‚˜λΌμ™€ 미ꡭ은 30fps λ₯Ό μ‚¬μš©ν•˜κ³  PAL & SECAM 방식을 μ‚¬μš©ν•˜λŠ” μœ λŸ½μ€ 25fps λ₯Ό μ‚¬μš©ν•˜κ³  μžˆλ‹€. μ™œμΌκΉŒ? μš°λ¦¬μ™€ 미ꡭ의 ꡐλ₯˜ μ „λ ₯ μ£ΌνŒŒμˆ˜λŠ” 60Hz κ³ , μœ λŸ½μ€ 50Hz λ₯Ό μ‚¬μš©ν•œλ‹€. ꡐλ₯˜ μ „λ ₯의 μ£ΌνŒŒμˆ˜μ™€ λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨μ„ 동기화 μ‹œν‚€λ©΄ λΉ„μš©μ„ 쀄일 수 μžˆλŠ” 점에 μ°©μ•ˆν•œ 것이닀. κ°„ν˜Ή κ°€λ‹€ 50Hz λͺ¨λ‹ˆν„°λ„ μžˆμ§€λ§Œ λŒ€λΆ€λΆ„μ˜ λ””μŠ€ν”Œλ ˆμ΄κ°€ 60Hz κ°€ ν‘œμ€€μœΌλ‘œ 자리 μž‘μ€ μ΄μœ λ‹€.

μ—¬κΈ°μ„œ λ¬Έμ œκ°€ λ˜λŠ” 것이 λ¬΄μ—‡μΌκΉŒ? 디지털 방솑이 μ•„λ‹Œ μ˜ν™”λ₯Ό TV둜 λ³Ό λ•Œ λ°œμƒν•œλ‹€. 60HzλŠ” 1μ΄ˆμ— 60번 ν”„λ ˆμž„μ„ λ³€κ²½ν•  수 μžˆλ‹€λŠ” 말인데 μ˜ν™”λŠ” μ•½ 24fps 이기 λ•Œλ¬Έμ— μ •μˆ˜λ°°λ₯Ό 맞좜 수 μ—†λ‹€. κ²°κ΅­ 각각의 ν”„λ ˆμž„μ΄ 3번, 2번, 3번, 2λ²ˆβ€¦ μ΄λŸ°μ‹μœΌλ‘œ λΏŒλ €μ Έμ•Ό 60Hz 쑰건을 맞좜 수 μžˆλŠ” 것이닀. 그러면 무슨 일이 λ°œμƒν• κΉŒ? μ˜μƒμ΄ μ–΄μƒ‰ν•œ μˆœκ°„μ΄ λ‚˜νƒ€λ‚˜κ²Œ λœλ‹€. μ•ˆκ·Έλž˜λ„ 24fps λΌλŠ” ν”„λ ˆμž„ μˆ˜μΉ˜κ°€ 높은 μˆ˜μΉ˜κ°€ μ•„λ‹Œλ° ν”„λ ˆμž„ λΆ„λ°° λ§ˆμ € μ •μˆ˜λ°°κ°€ λ§žμ§€ μ•ŠμœΌλ‹ˆ μ˜μƒμ΄ λ§€λ„λŸ½μ§€ μ•Šμ€ λ¬Έμ œκ°€ λ°œμƒν•œλ‹€.

κ·Έλž˜μ„œ TV λŠ” νŒ¨λ„μ— 120Hz 의 μ£Όμ‚¬μœ¨μ„ λ„μž…ν–ˆλ‹€. 그러면 24fps 의 μ •μˆ˜λ°°κ°€ 되기 λ•Œλ¬Έμ΄λ‹€. κ²Œλ‹€κ°€ AD λ³΄λ“œλ₯Ό μ‚¬μš©ν•΄ ν”„λ ˆμž„ λ³΄κ°„κΉŒμ§€ ν•˜λŠ” λ“± μ˜μƒμ„ λΆ€λ“œλŸ½κ²Œ ν•˜κΈ° μœ„ν•΄ μ΅œμ„ μ„ λ‹€ν•˜κ³ μžˆλ‹€.

ν”„λ ˆμž„ 배뢄이 κ³ λ₯΄κ²Œ λ˜μ§€ μ•ŠλŠ”λ‹€λŠ” 것은
λ§€λ„λŸ¬μš΄ 화면을 λ³Ό 수 μ—†λ‹€λŠ” 것을 μ˜λ―Έν•œλ‹€.

3. In Browser

이제 λ‹€μ‹œ λΈŒλΌμš°μ €λ‘œ λŒμ•„μ™€λ³΄μž. TV μ‚¬λ‘€λ‘œλΆ€ν„° 60Hz 의 λͺ¨λ‹ˆν„°μ—μ„œ κ°€μž₯ 졜적의 μ• λ‹ˆλ©”μ΄μ…˜μ€ 60fps λ₯Ό κ°€μ Έμ•Ό 함을 μ•Œ 수 μžˆλ‹€. κ·Έ λ‹€μŒ λŒ€μ•ˆμœΌλ‘œλŠ” μ •μˆ˜λ°°λ₯Ό κ°€μ§ˆ 수 μžˆλŠ” κ°’ 쀑 κ°€μž₯ 큰 κ°’μœΌλ‘œ 30fps κ°€ λŒ€μ•ˆμ΄ 될 수 μžˆλ‹€.

그러면 setInterval 둜 60fps λ₯Ό λ§Œμ‘±μ‹œν‚€λ €λ©΄ μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ?

60fps λ₯Ό λ’€μ§‘μ–΄λ³΄μž.

\( \frac{1\, \text{s}}{60\, \text{frames}} = \frac{1000\, \text{ms}}{60\, \text{frames}} \approx 16.6\, \text{ms} \)

각 ν”„λ ˆμž„κ°„ 간격이 μ•½ 16.6ms κ°€ λ˜μ–΄μ•Ό ν•œλ‹€. λ§Œμ•½ 30fps 둜 μ• λ‹ˆλ©”μ΄μ…˜μ„ 뿌리고 μ‹Άλ‹€λ©΄ ν”„λ ˆμž„ 간격은 2배인 33.2ms κ°€ λ˜λŠ” 것이닀.

setInterval 이 μ •ν™•νžˆ μ£ΌκΈ°λ§ˆλ‹€ μ‹€ν–‰λœλ‹€λ©΄ μ•„λ¬΄λŸ° 문제 없이 μ• λ‹ˆλ©”μ΄μ…˜μ„ 좜λ ₯ν•  수 μžˆλ‹€. ν•˜λ“œμ›¨μ–΄ 가속이 μ§€μ›λ˜μ§€ μ•Šμ•„ λ‹€μ†Œ μ„±λŠ₯에 영ν–₯을 λ―ΈμΉ˜λ”λΌλ„ μ• λ‹ˆλ©”μ΄μ…˜ κ΅¬ν˜„ μžμ²΄λŠ” μ •μƒμ μœΌλ‘œ ν•  수 μžˆλ‹€λŠ” 말이 λœλ‹€.

그런데 λ¬Έμ œλŠ” setInterval은 λΈŒλΌμš°μ €μ˜ λ Œλ”λ§ 주기와 λ¬΄κ΄€ν•˜κ²Œ μž‘λ™ν•œλ‹€λŠ” 것이닀. 즉, λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨μ„ κ³ λ €ν•˜μ§€ μ•Šκ³  μ‹€ν–‰λœλ‹€λŠ” 말이닀. 이게 μ™œ λ¬Έμ œκ°€ 될까?

μš°μ„  첫 번째 λ¬Έμ œλŠ” λͺ¨λ“  λ””μŠ€ν”Œλ ˆμ΄κ°€ 60Hz λ₯Ό μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€. μ‚¬μš©μžκ°€ μž„μ˜λ‘œ μ£Όμ‚¬μœ¨μ„ λ³€κ²½ν–ˆμ„ μˆ˜λ„ 있고, 50Hz λΌλ˜κ°€ 75Hz, κ·Έ μ™Έ κ³ μ£Όμ‚¬μœ¨λ‘œ 90Hz, 120Hz, 144Hz 와 같이 λ””μŠ€ν”Œλ ˆμ΄λ§ˆλ‹€ λ‹€μ–‘ν•œ μ£Όμ‚¬μœ¨μ„ κ°–λŠ”λ‹€. 두 번째 λ¬Έμ œλŠ” setInterval이 Macrotask Queue 라 Microtask Queue λ“± λ‹€λ₯Έ μž‘μ—…μ΄ μŠ€νƒμ— 끼어듀 경우 λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨κ³Ό setInterval 의 μ£ΌκΈ°κ°€ λ‹¬λΌμ§„λ‹€λŠ” 것이닀.


3. Synchronize with Display Hertz πŸ‘©β€πŸ’»

μœ„μ™€ 같은 문제λ₯Ό ν•΄κ²°ν•΄ μ›ν™œν•œ μ• λ‹ˆλ©”μ΄μ…˜μ„ ν‘œν˜„ν•˜κΈ° μœ„ν•œ 방법이 두 가지 μ‘΄μž¬ν•œλ‹€. ν•˜λ‚˜λŠ” CSS 의 animation을 μ‚¬μš©ν•˜λŠ” 것이고, λ‹€λ₯Έ ν•˜λ‚˜λŠ” JavaScript 의 requestAnimationFrame을 μ‚¬μš©ν•˜λŠ” 것이닀. 이 λ‘˜μ€ λΈŒλΌμš°μ €κ°€ μ œκ³΅ν•˜λŠ” API λ₯Ό μ‚¬μš©ν•΄ μ΅œμ ν™”λœ μ• λ‹ˆλ©”μ΄μ…˜μ„ λ§Œλ“€μ–΄λ‚΄λŠ”λ° νŠΉμ§•μ€ λ‹€μŒκ³Ό κ°™λ‹€.

  1. λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨(=λΈŒλΌμš°μ € λ Œλ”λ§ 엔진 μ‹€ν–‰ μ£ΌκΈ°)와 동기화 돼 μ‹€ν–‰λ˜λ―€λ‘œ ν”„λ ˆμž„ 손싀 λ°œμƒ κ°€λŠ₯성이 적닀.
  2. λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨κ³Ό λ™κΈ°ν™”λ˜λ―€λ‘œ 졜적의 μ• λ‹ˆλ©”μ΄μ…˜ ν”„λ ˆμž„ κ΅¬ν˜„μ΄ κ°€λŠ₯ν•˜λ‹€.
  3. ν•˜λ“œμ›¨μ–΄ 가속을 μ‚¬μš©ν•  수 μžˆλ‹€.

κ·Έλ ‡κΈ° λ•Œλ¬Έμ— 이 두 방식은 더 λ›°μ–΄λ‚œ μ• λ‹ˆλ©”μ΄μ…˜μ„ μ œκ³΅ν•  수 μžˆλŠ” 것이닀.

1. CSS - animation

1 ) Animation Properties

CSS 의 animation이 μ œκ³΅ν•˜λŠ” properties λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

  • animation-name: @keyframes 이름을 μ§€μ •ν•˜λ©° ν•„μˆ˜κ°’μ΄λ‹€.
  • animation-duration: ms, s λ‹¨μœ„λ‘œ μ§€μ •ν•˜λ©° ν•„μˆ˜κ°’μ΄λ‹€.
  • animation-timing-function: μ• λ‹ˆλ©”μ΄μ…˜ 타이밍 ν•¨μˆ˜λ₯Ό μ§€μ •ν•œλ‹€. 기본값은 easeλ‹€.
  • animation-delay: ms, s λ‹¨μœ„λ‘œ μ• λ‹ˆλ©”μ΄μ…˜ μ‹œμž‘ λ”œλ ˆμ΄λ₯Ό μ§€μ •ν•œλ‹€.
  • animation-iteration-count: 기본값은 1이며 infinite을 μ£Όλ©΄ λ¬΄ν•œ μž¬μƒμ΄ κ°€λŠ₯ν•˜λ‹€.
  • animation-direction: 기본값은 normal이며, reverseλŠ” μ—­λ°©ν–₯, alternateλŠ” μ •λ°©ν–₯ λ°”μš΄μŠ€, alternate-reverseλŠ” μ—­λ°©ν–₯ λ°”μš΄μŠ€λ₯Ό μ§€μ •ν•œλ‹€.
  • animation-fill-mode: μ• λ‹ˆλ©”μ΄μ…˜ 적용 ν–‰λ™κ°’μœΌλ‘œ 기본값은 normal이닀.
    - normal: λŒ€κΈ° -> μ‹œμž‘ -> μ’…λ£Œ -> λŒ€κΈ°
    - forwards: λŒ€κΈ° -> μ‹œμž‘ -> μ’…λ£Œ
    - backwards: μ‹œμž‘ -> μ’…λ£Œ -> λŒ€κΈ°
    - both: μ‹œμž‘ -> μ’…λ£Œ
  • animation-play-state: 기본값은 running이며 pausedλ₯Ό μ£Όμ–΄ μΌμ‹œμ •μ§€ μ‹œν‚¬ 수 μžˆλ‹€.

참고둜 setInterval 은 clearInterval 둜 λ°˜λ³΅μ„ 쀑단할 수 μžˆλ“―μ΄ requestAnimationFrame 은 cancelAnimationFrame 둜 μ• λ‹ˆλ©”μ΄μ…˜ 쀑단이 κ°€λŠ₯λ‹€. ν•˜μ§€λ§Œ 이것은 λ°˜λ³΅μ„ μ€‘λ‹¨μ‹œν‚€λŠ” 것일 뿐 μž¬κ°œν•  수 μžˆλŠ” κΈ°λŠ₯은 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ‹€. λ”°λΌμ„œ 재개λ₯Ό μœ„ν•΄μ„œλŠ” λ‹€μ‹œ μ‹œμž‘ν•  λ–Ό 남은 μ‹œκ°„κ³Ό λ³€ν™”λŸ‰λ§Œ μ• λ‹ˆλ©”μ΄μ…˜μ„ μ§„ν–‰ν•˜λ„λ‘ 계산을 직접 ν•΄μ£Όμ–΄μ•Όν•œλ‹€. 반면 animation 의 animation-play-stateλŠ” 값을 λ°”κΎΈλŠ” 것 만으둜 μΌμ‹œμ •μ§€ 및 재개λ₯Ό ν•  수 μžˆλ‹€.

그리고 μœ„ properties 단좕 μ†μ„±μœΌλ‘œ animation을 μ‚¬μš©ν•˜λ©΄ λͺ¨λ“  섀정값을 ν•œ λ²ˆμ— 지정할 수 μžˆλ‹€.

animation: name duration timing-function delay iteration-count direction fill-mode play-state;


2 ) @keyframes

μœ„ properties λŠ” μ• λ‹ˆλ©”μ΄μ…˜ 적용 μŠ€νƒ€μΌμ„ μ§€μ •ν•˜λŠ” 뢀뢄은 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ‹€. 즉, μ• λ‹ˆλ©”μ΄μ…˜ 섀정값을 μ§€μ •ν•˜λŠ” μ˜΅μ…˜μœΌλ‘œ, μ‹€μ œ μŠ€νƒ€μΌμ€ @keyframesλ₯Ό μ‚¬μš©ν•΄ μ§€μ •ν•œλ‹€. λ”°λΌμ„œ animation은 λ°˜λ“œμ‹œ @keyframesλ₯Ό μž‘μ„±ν•΄μ•Όν•¨μ„ μ˜λ―Έν•˜λŠ” 것이닀.

@keyframesμ—μ„œ μ‚¬μš©ν•  수 μžˆλŠ” properties λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

  • from: μ‹œμž‘ μŠ€νƒ€μΌμ„ 지정.
  • to: μ’…λ£Œ μŠ€νƒ€μΌμ„ 지정.

참고둜 μ—¬κΈ°μ„œ μ‹œμž‘κ³Ό μ’…λ£ŒλŠ” μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ‹€ν–‰λ˜λŠ” μ‹œμ μ— μ‹œμž‘κ³Ό μ’…λ£Œλ₯Ό μ˜λ―Έν•œλ‹€. μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ‹œμž‘λ˜κΈ° μ „ λŒ€κΈ° μƒνƒœλ₯Ό μ˜λ―Έν•˜λŠ” 것이 μ•„λ‹˜μ— μœ μ˜ν•˜λ„λ‘ ν•œλ‹€.

  • %: from, toλŠ” 0% 와 100% λ₯Ό μ‚¬μš©ν•œ 것과 κ°™λ‹€. λ§Œμ•½ 더 λ‹€μ–‘ν•œ κ΅¬κ°„μœΌλ‘œ λ‚˜λˆ„κ³  μ‹Άλ‹€λ©΄ %λ₯Ό μ‚¬μš©ν•΄ μ—¬λŸ¬ 단계별 μ• λ‹ˆλ©”μ΄μ…˜ 지정이 κ°€λŠ₯ν•˜λ‹€.

2. JavaScript - requestAnimationFrame

const element = document.getElementById("some-element-you-want-to-animate");
let start, previousTimeStamp;
let done = false;

function step(timeStamp) {
  if (start === undefined) {
    start = timeStamp;
  }
  const elapsed = timeStamp - start;

  if (previousTimeStamp !== timeStamp) {
    // Math.min() is used here to make sure the element stops at exactly 200px
    const count = Math.min(0.1 * elapsed, 200);
    element.style.transform = `translateX(${count}px)`;
    if (count === 200) done = true;
  }

  if (elapsed < 2000) {
    // Stop the animation after 2 seconds
    previousTimeStamp = timeStamp;
    if (!done) {
      window.requestAnimationFrame(step);
    }
  }
}

window.requestAnimationFrame(step);

requestAnimationFrame 은 window 객체가 가지고 μžˆλŠ” κΈ€λ‘œλ²Œ API λ‹€. 이 λ©”μ„œλ“œμ— ν”„λ ˆμž„μ„ 그릴 ν•¨μˆ˜λ₯Ό μ „λ‹¬ν•¨μœΌλ‘œμ¨ μž‘λ™ν•˜λŠ” 데, 단 ν•˜λ‚˜μ˜ ν”„λ ˆμž„λ§Œ 그리기 λ•Œλ¬Έμ— μ• λ‹ˆλ©”μ΄μ…˜μ΄ μž‘λ™ν•˜κΈ° μœ„ν•΄μ„œλŠ” ν•¨μˆ˜ λ‚΄μ—μ„œ λ‹€μ‹œ μžμ‹ μ„ requestAnimationFrame에 μ „λ‹¬ν•˜λ„λ‘ ν•΄ μž¬κ·€ν•¨μˆ˜λ₯Ό μž‘μ„±ν•˜λŠ” 것과 μœ μ‚¬ν•˜κ²Œ μ½”λ“œλ₯Ό μž‘μ„±ν•΄μ•Όν•œλ‹€.

CSS animationλŠ” 상관 μ—†μ§€λ§Œ JavaScript requestAnimationFrame은 μ• λ‹ˆλ©”μ΄μ…˜μ„ μœ„ν•΄ JavaScript 콜백 ν•¨μˆ˜λ₯Ό μž‘μ„±ν•΄μ•Όν•œλ‹€. 그리고 이 μ½œλ°±ν•¨μˆ˜κ°€ 16.6ms λ₯Ό μ΄ˆκ³Όν•˜λŠ” 무거운 ν•¨μˆ˜μΌ 경우 ν”„λ ˆμž„ 손싀이 λ°œμƒν•  수 μžˆμŒμ— μœ μ˜ν•΄μ•Όν•œλ‹€.

참고둜 setInterval 은 clearInterval 둜 λ°˜λ³΅μ„ 쀑단할 수 μžˆλ“―μ΄ requestAnimationFrame 은 cancelAnimationFrame 둜 μ• λ‹ˆλ©”μ΄μ…˜ 쀑단이 κ°€λŠ₯ν•˜λ‹€.

Timing Function 을 μ μš©ν•΄μ•Ό ν•  경우 CSS 의 animation을 μ‚¬μš©ν•˜κ±°λ‚˜, JavaScript λ₯Ό μ‚¬μš©ν•  경우 GSAP, Framer-motionκ³Ό 같은 라이브러리λ₯Ό μ‚¬μš©ν•΄μ•Όν•˜μ§€λ§Œ linear ν•œ 기본적인 μ• λ‹ˆλ©”μ΄μ…˜μ€ λ³„λ„μ˜ 라이브러리 μ„€μΉ˜ 없이 requestAnimationFrame을 직접 κ΅¬ν˜„ν•΄ κ°€λ³κ²Œ μ‚¬μš©ν•  수 μžˆλ‹€λŠ” 것이 κ°€μž₯ 큰 μž₯점이닀.

그리고 μœ„ μ½”λ“œλ₯Ό 보면 μ½œλ°±ν•¨μˆ˜κ°€ λ””μŠ€ν”Œλ ˆμ΄ 주파수만큼 μ‹€ν–‰λ˜κΈ° λ•Œλ¬Έμ— 얼핏 보면 λ„ˆλ¬΄ λ§Žμ€ μŠ€νƒμ΄ μŒ“μ—¬ Stack Overflowκ°€ λ°œμƒν•  것 κ°™μ§€λ§Œ μ•ˆμ „ν•˜λ‹€λŠ” 것이닀! requestAnimationFrame에 μ‚¬μš©λ˜λŠ” μ½œλ°±ν•¨μˆ˜λŠ” μ»΄νŒŒμΌλŸ¬μ— μ˜ν•œ 꼬리 μž¬κ·€(Tail Recursion) μ΅œμ ν™”μ™€ λ§ˆμ°¬κ°€μ§€λ‘œ λΈŒλΌμš°μ €κ°€ λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨κ³Ό 동기화 λ˜μ–΄ λ°˜λ³΅λ¬Έμ„ μˆ˜ν–‰ν•˜λ“―μ΄ μž‘λ™ν•˜κΈ° λ•Œλ¬Έμ— κ³Όλ„ν•œ 콜백 ν•¨μˆ˜λ‘œ μΈν•œ Stack Overflow 문제λ₯Ό μΌμœΌν‚€μ§€ μ•ŠλŠ”λ‹€.


그리고 requeatAnimationFrame은 animationκ³Ό κ°€μž₯ μ€‘μš”ν•œ 차이점이 ν•˜λ‚˜ μ‘΄μž¬ν•œλ‹€. λ‹€λ₯Έ JavaScript 에 μ˜ν•΄ λ©”μΈμŠ€λ ˆλ“œκ°€ λΈ”λ‘œν‚Ή λ˜λ”λΌλ„ μ• λ‹ˆλ©”μ΄μ…˜μ΄ λ¦¬λ Œλ”λ§λ§Œ λ˜μ§€ μ•Šμ„ 뿐 λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨κ³Ό 동기화 λ˜μ–΄ 계속 μ‹€ν–‰λ˜λŠ”κ²ƒμ€ λ™μΌν•˜λ‹€.
ν•˜μ§€λ§Œ ν•΄λ‹Ή 탭이 ν¬κ·ΈλΌμš΄λ“œμ— μžˆμ§€ μ•Šμ„ κ²½μš°μ—λ„ 계속 μž¬μƒλ˜λŠ” animationκ³Ό 달리 requeatAnimationFrame은 μ• λ‹ˆλ©”μ΄μ…˜μ„ μΌμ‹œ μ •μ§€ν•œλ‹€. 이것은 λͺ¨λ°”일 기기와 같은 ν™˜κ²½μ—μ„œ λ¦¬μ†ŒμŠ€λ₯Ό μ ˆμ•½ν•  수 μžˆλ‹€.

Β  setInterval animation requestAnimationFrame
λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨ 동기화 X O O
ν•˜λ“œμ›¨μ–΄ 가속 X O O
λ©”μΈμŠ€λ ˆλ“œ λΈ”λ‘œν‚Ή μ‹œ μ• λ‹ˆλ©”μ΄μ…˜ μž¬μƒ λΈ”λ‘œν‚Ή ν•΄μ œ ν›„ μ΄μ–΄μ„œ 진행 λ¦¬λ Œλ”λ§μ€ μ•ˆ λ˜μ§€λ§Œ μ• λ‹ˆλ©”μ΄μ…˜μ€ λΈ”λ‘œν‚Ή 없이 진행 λ¦¬λ Œλ”λ§μ€ μ•ˆ λ˜μ§€λ§Œ μ• λ‹ˆλ©”μ΄μ…˜μ€ λΈ”λ‘œν‚Ή 없이 진행
탭이 ν¬κ·ΈλΌμš΄λ“œκ°€ 아닐 λ•Œ μ• λ‹ˆλ©”μ΄μ…˜ μž¬μƒ μž¬μƒ μž¬μƒ μΌμ‹œμ •μ§€

4. Examples - Progress Bar πŸ‘©β€πŸ’»

1. setInterval

import { $ } from '/assets/js/utils/render.js';
import { delay } from '/assets/js/utils/fp.js';


const HERTZ = 60;
const TWO_SECONDS = 2;
const FRAME_INTERVAL = 1000 / HERTZ; // 16.6ms

const progress = $('.progress');

let runner;

const start = () => {
  const changeQuantity = 100;
  const totalFrames = HERTZ * TWO_SECONDS;
  const oneFrameChangeQuantity = changeQuantity / totalFrames;
  let frame = 0;

  runner = setInterval(() => {
    progress.style.width = `${oneFrameChangeQuantity * ++frame}%`;
    if (frame === totalFrames) stop();
  }, FRAME_INTERVAL);
};
const stop = () => {
  clearInterval(runner);
  attachStartEvent();
};
const task = async (event) => {
  event.target.style.backgroundColor = '#a9a9a9';
  let sum = 0;
  await delay(10);
  while (true) {
    sum += 1;
    if (sum > 3_000_000_000) break;
  }
  event.target.style.backgroundColor = '#ffe4c4';
};

const attachStartEvent = () => {
  $('#start').addEventListener('click', start, { once: true });
};
attachStartEvent();
$('#stop').addEventListener('click', stop);
$('#task').addEventListener('click', task);

μœ„μ— μ‚¬μš©λœ $λŠ” jQuery κ°€ μ•„λ‹ˆκ³  querySelectorAll, querySelectorλ₯Ό ν•˜λ‚˜λ‘œ 합쳐 λ§Œλ“  ν•¨μˆ˜λ‘œ render.js μ—μ„œ 확인할 수 μžˆλ‹€.

β€˜μ‹€ν–‰β€™μ„ 눌러보면 λ§‰λŒ€κΈ°κ°€ ν•œ λ²ˆμ”© ν”„λ ˆμž„μ΄ μ–΄κΈ‹λ‚˜ λΆ€λ“œλŸ½μ§€ μ•Šκ²Œ μ˜¬λΌκ°€λŠ” 것을 λ³Ό 수 μžˆλ‹€. λ˜ν•œ λ§‰λŒ€κΈ°κ°€ μ¦κ°€ν•˜λŠ” 도쀑 β€˜λ¬΄κ±°μš΄ μž‘μ—…β€™μ„ λˆ„λ₯΄λ©΄ μΌμ‹œμ •μ§€ λ˜μ—ˆλ‹€ 무거운 μž‘μ—…μ΄ μ’…λ£Œλ˜κ³  μŠ€νƒμ΄ λΉ„κ²Œ 되면 λ‹€μ‹œ λ‚˜λ¨Έμ§€ μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ§„ν–‰λœλ‹€.

2. animation

.progress {
  height: 100%;
  width: 0;
  background-color: #31e51f;

  animation-name: makeFull;
  animation-duration: 2s;
  animation-timing-function: ease;
  animation-iteration-count: infinite;
  animation-direction: normal;
  animation-fill-mode: forwards;
  animation-play-state: paused;
}

@keyframes makeFull {
  from {
    width: 0;
  }
  to {
    width: 100%;
  }
}
timing-function
direction

μš°μ„  κ°€μž₯ λˆˆμ— λ„λŠ” 것은 ν”„λ ˆμž„ 손싀 없이 맀우 λΆ€λ“œλŸ¬μš΄ μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ μš©λœλ‹€λŠ” 것이닀. β€˜μ€‘λ‹¨β€™μ„ λˆŒλ €λ‹€ β€˜μ‹€ν–‰β€™μ„ 눌러보면 μ• λ‹ˆλ©”μ΄μ…˜μ΄ μΌμ‹œμ •μ§€ λ˜μ—ˆλ‹€ μž¬κ°œλ˜λŠ” 것을 λ³Ό 수 μžˆλ‹€.

그리고 β€˜λ¬΄κ±°μš΄ μž‘μ—…β€™μ„ λˆ„λ₯΄λ©΄ μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ •μ§€λœλ‹€. λΈŒλΌμš°μ €μ˜ λ¦¬λ Œλ”λ§(Reflow, Repaint)이 λ©”μΈμŠ€λ ˆλ“œμ—μ„œ μ •μ§€λ˜κΈ° λ•Œλ¬Έμ΄λ‹€. ν•˜μ§€λ§Œ μœ„ setInterval κ³ΌλŠ” μ€‘μš”ν•œ 차이점이 μžˆλŠ”λ°, λΈŒλΌμš°μ €μ˜ λ¦¬λ Œλ”λ§λ§Œ 정지될 뿐 CSS μ• λ‹ˆλ©”μ΄μ…˜μ€ μ€‘λ‹¨λ˜μ§€ μ•ŠλŠ”λ‹€λŠ” 것이닀.
κ·Έλ ‡κΈ° λ•Œλ¬Έμ— 무거운 μž‘μ—…μ΄ μ’…λ£Œλ˜κ³  λ¦¬λ Œλ”λ§μ΄ 될 λ•Œ CSS μ• λ‹ˆλ©”μ΄μ…˜μ€ λ…λ¦½μ μœΌλ‘œ μ²˜λ¦¬ν•˜λ˜ ν”„λ ˆμž„μ—μ„œ λ¦¬λ Œλ”λ§μ΄ μ‹œμž‘λœλ‹€.

즉, μ‹œκ°μ μœΌλ‘œ λ¦¬λ Œλ”λ§μ€ λ˜μ§€ μ•Šμ§€λ§Œ μ• λ‹ˆλ©”μ΄μ…˜ ν”„λ ˆμž„ μžμ²΄λŠ” JavaScript 에 μ „ν˜€ 영ν–₯을 받지 μ•ŠλŠ”λ‹€.

3. requestAnimationFrame

μ—¬κΈ°μ„œ μ‚¬μš©λœ Animation 은 requestAnimationFrame 을 μ‚¬μš©ν•˜κΈ° 쉽도둝 Class λ₯Ό λ§Œλ“  κ²ƒμœΌλ‘œ μ½”λ“œλŠ” Animation.js μ—μ„œ 확인할 수 μžˆλ‹€.

import { $ } from '/assets/js/utils/render.js';
import { delay } from '/assets/js/utils/fp.js';
import Animation from '/assets/js/utils/Animation.js';

const TWO_SECONDS = 2;

const animation = new Animation($('.progress'));

const start = async () => {
  await animation.from({ width: '0%' }).to({ width: '100%' }).run(TWO_SECONDS);
  attachStartEvent();
};
const stop = () => {
  animation.stop();
  attachStartEvent();
};
const task = async (event) => {
  event.target.style.backgroundColor = '#a9a9a9';
  let sum = 0;
  await delay(10);
  while (true) {
    sum += 1;
    if (sum > 3_000_000_000) break;
  }
  event.target.style.backgroundColor = '#ffe4c4';
};

const attachStartEvent = () => {
  $('#start').addEventListener('click', start, { once: true });
};
attachStartEvent();
$('#stop').addEventListener('click', stop);
$('#task').addEventListener('click', task);

animationκ³Ό λ§ˆμ°¬κ°€λ‘œ ν”„λ ˆμž„ 손싀 없이 맀우 λΆ€λ“œλŸ¬μš΄ μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ μš©λ˜λŠ” 것을 확인할 수 μžˆλ‹€.

그리고 β€˜λ¬΄κ±°μš΄ μž‘μ—…β€™μ„ 눌러보면 animation κ³Ό λ§ˆμ°¬κ°€μ§€λ‘œ λΈŒλΌμš°μ €μ˜ λ¦¬λ Œλ”λ§λ§Œ 정지될 뿐 CSS μ• λ‹ˆλ©”μ΄μ…˜μ€ μ€‘λ‹¨λ˜μ§€ μ•ŠλŠ”λ‹€λŠ” 것을 μ•Œ 수 μžˆλ‹€. μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ§„ν–‰λ˜λŠ” 도쀑 λ‹€λ₯Έ 무거운 μž‘μ—…μ΄ λ©”μΈμŠ€λ ˆλ“œλ₯Ό 차지해 λ¦¬λ Œλ”λ§ μžμ²΄κ°€ λ©ˆμΆ”λŠ” 것은 막을 수 μ—†μ§€λ§Œ μ• λ‹ˆλ©”μ΄μ…˜ μžμ²΄λŠ” μ •ν™•ν•œ μ• λ‹ˆλ©”μ΄μ…˜ 싀행을 보μž₯받을 수 μžˆμŒμ„ μ˜λ―Έν•œλ‹€.

단, μ£Όμ˜ν•΄μ•Ό ν•  것은 μ•žμ—μ„œλ„ λ§ν–ˆλ“―μ΄ requestAnimationFrame의 콜백 ν•¨μˆ˜κ°€ 16.6ms λ₯Ό μ΄ˆκ³Όν•΄ λŒμ•„κ°€λ©΄ μ•ˆ λœλ‹€. μ• λ‹ˆλ©”μ΄μ…˜ 싀행은 보μž₯ν•  수 μžˆμ§€λ§Œ λ””μŠ€ν”Œλ ˆμ΄ μ£Όμ‚¬μœ¨μ— λ¦¬λ Œλ”λ§μ„ ν•  수 μ—†μ–΄ 일뢀 ν”„λ ˆμž„μ΄ μ†μ‹€λ˜λ©° 그렀지기 λ•Œλ¬Έμ΄λ‹€.


5. Examples - Animation Composition πŸ‘©β€πŸ’»

1. setInterval

μ΄λ²ˆμ—λŠ” μ• λ‹ˆλ©”μ΄μ…˜μ„ μ—°κ²°ν•΄λ³΄μž.

import { $ } from './render.js';
import { delay } from '/assets/js/utils/fp.js';

const HERTZ = 60;
const TWO_SECONDS = 2;
const FRAME_INTERVAL = 1000 / HERTZ; // 16.6ms

const box = $('.board .box');

const start = async () => {
  const changeQuantity = 300;
  const totalFrames = HERTZ * TWO_SECONDS;
  const oneFrameChangeQuantity = changeQuantity / totalFrames;
  let frame;

  const toRight = () => {
    frame = 0;
    runner = setInterval(() => {
      box.style.left = `${oneFrameChangeQuantity * ++frame}px`;
      if (frame === totalFrames) clearInterval(runner);
    }, FRAME_INTERVAL);
  };
  const toBottom = () => {
    frame = 0;
    runner = setInterval(() => {
      box.style.top = `${oneFrameChangeQuantity * ++frame}px`;
      if (frame === totalFrames) clearInterval(runner);
    }, FRAME_INTERVAL);
  };
  const toLeft = () => {
    frame = 0;
    runner = setInterval(() => {
      box.style.left = `${oneFrameChangeQuantity * (totalFrames - ++frame)}px`;
      if (frame === totalFrames) clearInterval(runner);
    }, FRAME_INTERVAL);
  };
  const toTop = () => {
    frame = 0;
    runner = setInterval(() => {
      box.style.top = `${oneFrameChangeQuantity * (totalFrames - ++frame)}px`;
      if (frame === totalFrames) clearInterval(runner);
    }, FRAME_INTERVAL);
  };

  toRight();
  await delay(TWO_SECONDS * 1000);
  toBottom();
  await delay(TWO_SECONDS * 1000);
  toLeft();
  await delay(TWO_SECONDS * 1000);
  toTop();
  await delay(TWO_SECONDS * 1000);
  toBottom();
  await delay(TWO_SECONDS * 1000);
  toRight();
  await delay(TWO_SECONDS * 1000);
  toTop();
  await delay(TWO_SECONDS * 1000);
  toLeft();
  await delay(TWO_SECONDS * 1000);
  start();
};
start();

λΆˆκ°€λŠ₯ν•œκ±΄ μ•„λ‹ˆμ§€λ§Œ ν”„λ ˆμž„λ„ μ €ν•˜λ  뿐 μ•„λ‹ˆλΌ μ½”λ“œ μž‘μ„± 및 μ œμ–΄κ°€ νž˜λ“€λ‹€. 심지어 μ΄λŸ°μ‹μœΌλ‘œ μ½”λ“œλ₯Ό μž‘μ„±ν•  경우 탭이 ν¬κ·ΈλΌμš΄λ“œλ₯Ό μœ μ§€ν•˜μ§€ μ•ŠμœΌλ©΄ μ‹¬κ°ν•œ μ—λŸ¬κ°€ λ°œμƒν•˜κΈ°λ„ ν•œλ‹€.

2. animation

.box {
  width: 100px;
  height: 100px;
  background-color: #31e51f;
  position: absolute;
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  animation-name: rotation;
  animation-duration: 8s;
  animation-timing-function: ease;
  animation-iteration-count: infinite;
  animation-direction: alternate;
  animation-play-state: running;
}

@keyframes rotation {
  0% {
    top: 0;
    left: 0;
    background-color: #31e51f;
  }
  25% {
    top: 0;
    left: 300px;
    background-color: #e5401f;
  }

  50% {
    top: 300px;
    left: 300px;
    background-color: #3662dc;
  }

  75% {
    top: 300px;
    left: 0;
    background-color: #e5d81f;
  }
  100% {
    top: 0;
    left: 0;
    background-color: #31e51f;
  }
}

3. requestAnimationFrame

import { $ } from '/assets/js/utils/render.js';
import { pipe } from '/assets/js/utils/fp.js';
import ColorAddon from '/assets/js/utils/ColorAddon.js';
import Animation from '/assets/js/utils/Animation.js';

const animation = new Animation($('.board .box'), { ColorAddon });

const topLeft = {
  top: '0px',
  left: '0px',
  backgroundColor: '#31e51f',
};
const topRight = {
  top: '0px',
  left: '300px',
  backgroundColor: '#e5401f',
};
const bottomRight = {
  top: '300px',
  left: '300px',
  backgroundColor: '#3662dc',
};
const bottomLeft = {
  top: '300px',
  left: '0px',
  backgroundColor: '#e5d81f',
};

const toRight = (self) => self.to(topRight).run(2);
const toBottom = (self) => self.to(bottomRight).run(2);
const toLeft = (self) => self.to(bottomLeft).run(2);
const toTop = (self) => self.to(topLeft).run(2);
const toBottomReversed = (self) => self.to(bottomLeft).run(2);
const toRightReversed = (self) => self.to(bottomRight).run(2);
const toTopReversed = (self) => self.to(topRight).run(2);
const toLeftReversed = (self) => self.to(topLeft).run(2);

const rotation = pipe(
  toRight,
  toBottom,
  toLeft,
  toTop,
  toBottomReversed,
  toRightReversed,
  toTopReversed,
  toLeftReversed
);

const run = async () => {
  await rotation(animation);
  run();
};
run();




Reference

  1. β€œWindow: requestAnimationFrame() method.” MDN Web Docs. Jan. 19, 2024, accessed Apr. 24, 2024, MDN - rAF.