토이프로젝트로 하고 있는 모두의파티 서비스에서 차트를 보여주는 것이 필요했다.
처음엔 차트 관련 라이브러리들을 이용하다가 커스텀 기능들이 마음에 들지 않아서
css-in-js 방식으로 직접 라이브러리를 만들고 있었다.
차트를 구성하는 선, 막대 형태들을 hook으로 만들어서 필요한 형태에 따라 선언하고
각 형태 hook에서는 css-in-js 방식이기 때문에 style 태그를 만들어서 document.head에 추가해준다.
스타일을 추가해주는 역할이기 때문에 useEffect보다 우선적으로 실행되어 동기적으로 처리되는 useLayoutEffect에서 관련 처리를 넣었다. 차트 관련 라이브러리를 만들다보니 이 훅을 많이 쓰게 되는 것 같다.
문제는 각 형태 hook에서 스타일을 계산하는데 간혹 디바이스 크기, 차트 데이터에 따라 차트 형태가 깨져버리는 버그가 발생했다.
코드가 많이 복잡하여 문제 부분만 수도코드로 표현하자면 아래와 같다.
function EveryoneChart() {
const line = useLine();
const bar = useBar();
return (
...
)
}
function useLine() {
useLayoutEffect(() => {
const styleEl = document.createElement('style');
...스타일 계산
document.head.appendChild(styleEl);
}, []);
}
function useBar() {
useLayoutEffect(() => {
const styleEl = document.createElement('style');
...스타일 계산
document.head.appendChild(styleEl);
}, []);
}
문제 원인
문제 원인은 useLayoutEffect에서 style 태그를 추가하는 행동이었다.
useLayoutEffect는 모든 DOM 업데이트가 일어난 후에 동기적으로 실행되는 hook으로써 useEffect보다 실행 순서가 앞선다.
따라서 나는 평소에 DOM을 동기적으로 건드려야 하는 일이 있다면 이 hook을 사용한다.
하지만 useLayoutEffect의 문제는 딱 한 번만 선언되어 사용되는 일이 아닐 때 발생한다.
위 코드처럼 여러 커스텀 훅에서 useLayoutEffect를 사용한다면 문제가 발생한다.
왜냐하면 한 커스텀훅에서 useLayoutEffect에서 각 형태에 사용되는 스타일 태그를 생성하여 DOM에 추가하는데
이렇게 되면 다른 커스텀 훅의 useLayoutEffect에서는 계산이 달라질 수 있다.
이런 Concurrent 이슈 때문에 전체 차트를 그리는 과정에서 예상하지 못한 버그가 발생한 것이다.
문제 해결
리액트 18에서는 이런 문제를 해결하기 위해 useInsertionEffect 훅을 새롭게 제공한다.
주요 특징을 설명하자면
DOM 업데이트가 일어나기 이전에 동기적으로 실행된다. 즉, useLayoutEffect보다 실행순서가 앞선다.
아래와 같이 수정하여 문제를 해결했다.
function EveryoneChart() {
const line = useLine();
const bar = useBar();
return (
...
)
}
function useLine() {
useInsertionEffect(() => {
const styleEl = document.createElement('style');
...스타일 계산
document.head.appendChild(styleEl);
})
useLayoutEffect(() => {
... 스타일 계산
}, []);
}
function useBar() {
useInsertionEffect(() => {
const styleEl = document.createElement('style');
...스타일 계산
document.head.appendChild(styleEl);
});
useLayoutEffect(() => {
... 스타일 계산
}, []);
}
DOM에 style 태그를 주입하는 코드를 useInsertionEffect 훅 내부로 옮겼고
그 이후에 일어나야 하는 레이아웃 계산은 그대로 useLayoutEffect 내부에서 처리하게 했다.
이렇게 버그를 해결하면서 동시에 전체 렌더링 성능이 vercel 성능 분석 기준으로 기존보다 17% 상승했는데
아쉽게도 useInsertionEffect 패치만 한 것이 아니라 requestAnimationFrame, requestIdleCallback 등을 사용하여 차트 관련 라이브러리의 성능을 전반적으로 개선한 후에 측정한 것이라 정확한 비교는 어렵다. (관련 포스트는 정리되는대로 올릴 생각이다.)
다만 style 태그를 동적으로 추가하는 것이 왜 성능에 악영향을 줄 수 밖에 없는지 생각할 수 있었다.
style 태그란 것은 css 규칙을 의미하는 것인데 css 규칙이란 건 모든 DOM 노드에 규칙을 강제하게 되기에 전체 레이아웃에 영향을 줄 수 밖에 없다. css-in-js처럼 컴포넌트가 그려지면서 style이 중간에 추가되는 것은 특히나 성능에 악영향을 주게될 수 밖에 없다.
css-in-js는 처음 발표되었을 때는 귀찮은 css 작업을 js 내부에서 작업하게 되어 정말 혁신이었지만
위와 같은 고질적인 성능 문제가 있기 때문에 사실 안정적인 서비스를 구축하게 될 땐 css-in-js보단 css 모듈 같은 것이 더 낫다고 본다. 특히나 요즘은 tailwind가 놀라울 정도로 발전하고 있기 때문에 css의 번거로움이 많이 해소되고 있어서 css-in-js를 잘 찾지 않게 되는 것 같다.
그럼에도 이번에 css-in-js 방식을 사용해보려는 이유는 리액트 네이티브 등을 활용해 앱에서도 사용할 것을 고려하고 있기 때문에 사용했다.
'프로젝트 > 모두의파티' 카테고리의 다른 글
모두의파티 서비스 인증 서버 설계(JWT) (1) | 2022.09.04 |
---|