css를 포함하는 StyleSheet는 js를 포함하는 script와는 상관없이 과정이 진행되기 때문이다.
CSS in JS
CSS in JS 방식의 라이브러리는 js를 사용하여 컴포넌트 단위로 스코프를 나누었다.
js 변수로 선언된 css를 실제 stylesheet로 만들고 랜덤 문자열로 이루어진 클래스명을 만들어 컴포넌트 끼리 학실하게 분리될 수 있게 한다. (@emotion/css와 같은 css in js 라이브러리를 다루면서 props를 통해 css를 변경할 때 html의 head 태그 안에 새로운 요소들이 추가되는 것을 볼 수 있다.)
게다가 Sass 문법까지 사용 가능하며 변수 사용이 가능하여 클래스 중첩으로 제어하던 부분도 제어할 수 있다.
문제점
장점이 많아보이지만 다음과 같은 단점이 있다.
Script의 코드 증가
StyleSheet의 Script 변환은 HTML 파싱에 사용되는 Script의 코드가 늘어났다는 것을 의미한다.
브라우저 렌더링이 StyleSheet와 Script로 나누어 병렬처리 되던 것이 오직 Script로 이루어짐에 따라 그만큼 속도가 느려진다.
FOUC (Flash of unstyled content)
또한 CSS가 먼저 제공되어 렌더링시 형태가 잡혀있는 기존 방식에 비해 컴포넌트가 렌더링 되며 형태가 잡히기 때문에 원형의 모습이 잠깐 노출(FOUC)된다. 이는 사용자 경험을 저하시킨다.
해결을 위해 빌드시, 사용하는 style을 뽑아서 StyleSheet를 생성해 올려주는 기능이 또 필요하다.
SSR의 문제가 겹친다면 더욱 해결하기 어렵다. (과거 nextjs에서 css in JS 방식을 사용하였을 때 해당 문제를 겪은 적이 있다.)
정리
css를 사용할지 css in js를 사용할지는 버그의 가능성과 성능 측면에서 고민해야할 것 같다.
성능 최적화 관련해서 공부하다보면 꼭 있는 내용이 requestAnimationFrame이다.
사용해 본 경험이 없어 사실상 죽은 지식이였는데, 이번 기회에 사용해보려고 공부를 해보니까 주로 스크롤 이벤트 최적화에 많이 사용한다고 한다.
스크롤 이벤트 최적화를 위해선 EventTarget.addEventListener()의 pasive 옵션과 requestAnimationFrame에 대한 이해가 필요하다.
passive
EventTarget.addEventListener() 함수의 세 번째 parameter인 options 객체에는 passive라는 옵션이 존재한다.
이는 이벤트 핸들러 내부에서 절대 preventDefault()를 호출하지 않을 것을 나타내는 boolean 값이다.
EventTarget.addEventListener()를 통해 등록된 이벤트는 컴포지터 스레드가 받는다. 이벤트가 발생하면 컴포지터 스레드는 메인스레드에 이벤트를 넘기고 reflow 또는 repaint가 발생하고 렌더링 파이프라인에 따라 리렌더링 되는 것이 일반적인 과정이다.
하지만 passive 값을 true로 할 경우 컴포지터 스레드에서 이벤트를 메인스레드에 넘기고 처리를 기다리지 않고 바로 Composite하여 새로운 프레임을 바로 합성하게 된다.
만약 핸들러 내부에 e.preventDefault()가 존재하여 이를 수행할 경우 메인스레드에서 해당 이벤트 발생시의 기본 동작을 막고 이벤트 핸들러를 수행해야 한다. 하지만 passive값을 true로 할 경우 e.preventDefault()가 핸들러 내부에 존재하지 않는다는 것이 보장되기 때문에 메인스레드의 처리를 기다리지 않고 바로 새로운 프레임을 합성할 수 있게 된다.
명시하지 않아도 최신 브라우저에선 문서 레벨 노드인 WindowDocumentDocument.body의 touchstart 이벤트와 touchmove에선 passive의 기본 값을 true로 적용하고 있다.
주기적으로 실행되는 애니메이션을 window.requestAnimationFrame()으로 최적화 해보자
requestAnimationFrame(callback)
브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트 하는 함수를 호출하게 한다. 이 메소드는 리페인트 이전에 실행할 함수를 인자로 받는다.
다음 리페인트에서 그 다음 프레임을 애니메이트 하려면 콜백 루틴이 반드시 스스로 requestAnimationFrame()을 호출해야 한다.
대부분의 브라우저에서 W3C 권장사항에 따라 디스플레이 주사율만큼 호출되게 된다.
callback
파라미터인 callback은 다음 리페인트를 위한 애니메이션을 업데이트 할 때 호출할 함수이다. 콜백함수에는 requestAnimationFrame()이 콜백함수 실행을 시작할 때의 시점을 나타내는 DOMHighResTimeStamp() 단일 인자를 전달한다.
반환 값
requestAnimationFrame()을 취소할 수 있는 고유 id인 long 정수값이 반환된다. window.cancelAnimationFrame()함수로 전달하여 취소할 수 있다.
차이점
setInterval과는 다음과 같은 차이점이 있다.
주사율만큼의 interval
setInterval을 사용하여 구현할 경우 interval을 손수 설정해주어야 한다. requestAnimationFrame은 주사율만큼의 Interval을 가지게 된다.(설정해줄 필요가 없다.)
동시 실행
여러개의 setInterval을 사용할 경우 콜백이 겹쳐져서 버벅임이 발생할 수 있다. requestAnimationFrame을 사용할 경우 계속 실행하기 위해선 내부 callback에서 반드시 재호출해야 하므로 여러개의 애니메이션을 써도 버벅이지 않는다.
비동기
위의 동시실행과 같은 이야기지만 setTimeout, setInterval은 마이크로 태스크큐에서 작동한다. 하지만 requestAnimationFrame은 Animation Frame에서 동작한다. 때문에 setInterval과 달리 callback이 유실될 가능성이 없다.
예시
1 2 3 4 5 6 7 8 9 10 11 12
let start = null; let hi = 0; functioncallback(timestamp) { if (!start) start = timestamp; console.log("cnt", hi++);
let times = [1000, 2000, 3000, 4000]; Promise.all(times.map((time) => timer(time))); console.log("complete");
// 1000 타이머 시작 // 2000 타이머 시작 // 3000 타이머 시작 // 4000 타이머 시작 // 1000 타이머 종료 // 2000 타이머 종료 // 3000 타이머 종료 // 4000 타이머 종료 // complete
forawait (let time of times) { timer(time); }
// 1000 타이머 시작 // 1000 타이머 종료 // 2000 타이머 시작 // 2000 타이머 종료 // 3000 타이머 시작 // 3000 타이머 종료 // 4000 타이머 시작 // 4000 타이머 종료 // complete
즉 for await of는 Promise간의 실행순서를 보장하지만 Promise.all은 그러지 못한다.
4. Promise.allSettled
Promise.allSettled는 Promise.all과 같은 기능을 하지만 Promise.all은 부분적으로 실패할 경우에 배열 전체의 실행이 중지되고 catch문으로 이동한다. 하지만 allSettled는 배열의 모든 Promise를 실행하고 상태와 결과값을 하나의 객체로 resolve문에 넘긴다.
Promise.race([promise1, promise2]).then((value) => { console.log(value); // two });
정리
순서가 보장되어야 하는가? => for await of 일부 Promise에서 에러가 발생할 경우 전체의 실행을 멈추고 싶은가? => Promise.all 일부 Promise에서 에러가 발생하더라도 에러가 발생한 일부 Promise만 따로 예외처리 해주고 싶은가? => Promise.allSettled 가장 먼저 실행이 된 Promise의 결과만 다루고 싶은가? => Promise.race