개요
1. useLayoutEffect
2. Usage
3. useLayoutEffect의 성능저하
4. SSR에서 사용이 가능한가?
✅ useLayoutEffect란?
📦 useLayoutEffect(Setup, dependencies?)
브라우저가 화면을 다시 채우기 전에 실행되는 useEffect이며, 성능 저하를 유발할 수 있어 가급적 useEffect를 사용할 것을 공식문서에서는 권장하고 있다.
- 리랜더링 시: (클린업함수가 정의되어있을 시)cleanup 함수(이전 값ㅋ) → setup함수(새 값) → ( 컴포넌트가DOM에서 제거되기 전) cleanup 함수 실행
- 의존성을 이전 값과 비교하여 실행할지 말지를 결정하며, 인수가 생략되었을 시 렌더링할 때마다 실행된다.
📦 Usage
대부분의 컴포넌트는 단순히 JSX를 반환하지 화면에서 어디에 위치하는지 크기는 얼마나되는지를 알 필요는 없다. 브라우저가 해당 컴포넌트의 레이아웃을 계산하고 화면에 그리기때문이다. 하지만 표시하려는 공간이 충분치 않은 경우에는 이를 다시 계산해서 아래에 나타내거나 다른 위치에 나타내야한다. 이때에는 현재 위치가 어디인지에 대한 값이 필요로하기에 2번 랜더링을 해야한다.
예시) ToolTip
1. Tooltip은 초기에 heigth = 0으로 랜더링이 된다. (Tooltip은 상단에 위치하지만 heigth이 0이므로 여유공간이 없다.)
2. React는 이를 DOM에 배치하고 useLayoutEffect에서 코드를 실행한다.
3. useLayoutEffect는 Tooltip의 높이를 측정하고 즉시 다시 랜더링을 한다.
4. 현재 높이에 맞춰 Tooltip의 배치가 다시 이루어져 올바르게 배치된다.
5. React가 DOM에서 이를 업데이트 하면 브라우저에 최종적으로 표시가 된다.
아래와 같은 네비게이션바를 만들 때 useEffect를 사용했을 경우에는 마지막 더보기(...) 부분에서 깜빡임이 발생하는 것을 알 수 있다. 이는 네비게이션바의 넓이를 계산하고 이를 state로 관리하기에 발생하는 문제점이다. 하지만 이러한 레이아웃과 관련된 상황에서 useLayoutEffect는 활용성이 높게 작용한다.
위와 같이 깜빡이는 현상을 방지할 수 있는 useLayoutEffect는 유용하기만할 것같은데 왜 리액트 공식홈페이지는 이를 성능저하가 일어날 수 있다고 할까요?
📦 useLayoutEffcet의 성능저하
위 내용을 보면 알 수 있듯이 useEffcet는 초기값이 세팅된 상태와 이후 변화되는 상태에 대해서 2개의 task로 분리해서 실행한다. 그리고 useLayoutEffect는 이를 1개의 task로 묶어서 처리하는 것을 알 수 있다.
이는 좀 더 자세히 알기 위해서 리액트에서 페인팅이라고 불리는 것에 대해 알아보겠다.
간단히 올빼미를 그리는 것을 통해서 페인팅을 알아보겠다.
페인팅 과정을 쉽게 설명하자면 계속해서 올빼미를 그렸다가 지우고 하는 것과는 다르게 어떠한 하나의 슬라이드를 보여주고, 올빼미에 대한 아이디어가 완성된 후 그 올빼미로 전환하는 방식으로 페인팅은 이루어진다.
이 말은 즉 아래와 같은 코드는 순서대로 실행이 되는 것이 아닌 한번에 실행이 된다는 소리이다. 이러한 페인팅은 13ms간격으로 시간 내에 브라우저 대기열에 있는 작업(task)를 가져와서 실행하여 화면을 새로고친다.
// child에 대해서 배경을 3번을 바꾸는 것을 알 수 있지만 실상 페인팅은 마지막만 반영해서 그린다
const app = document.getElementById("app");
const child = document.createElement("div");
child.innerHTML = "<h1>Heyo!</h1>";
app.appendChild(child);
child.style = "border: 10px solid red";
child.style = "border: 20px solid green";
child.style = "border: 30px solid black";
🎉 if 13ms보다 오래걸린다면??
const waitSync = (ms) => {
let start = Date.now(),
now = start;
while (now - start < ms) {
now = Date.now();
}
};
child.style = "border: 10px solid red";
waitSync(1000);
child.style = "border: 20px solid green";
waitSync(1000);
child.style = "border: 30px solid black";
waitSync(1000);
위와 같은 방식을 통해서 13ms보다 오래걸리게끔 조절을 해보았을 때 페인팅 차단, 랜더링 차단이라고 불리는 현상이 일어나며 이로 인해서 랜더링 자체가 늦어진다.
setTimeout(() => {
child.style = "border: 10px solid red";
wait(1000);
setTimeout(() => {
child.style = "border: 20px solid green";
wait(1000);
setTimeout(() => {
child.style = "border: 30px solid black";
wait(1000);
}, 0);
}, 0);
}, 0);
비동기처리를 통해서 task자체를 나눠서 실행되게 할 수 있다.
위의 내용을 종합했을 때 useLayoutEffect에서는 성능저하가 발생을 하게됩니다. 다만 요소의 실제 크기에 따라 UI를 조정해야하는 요소가 있을 때 (깜빡임이 있을 때, 시각적 결함이 있을 때)에 해결방법으로 제시될 수 있는 것이 useLayoutEffect가 되겠습니다. 그렇기에 대부분의 경우에 useEffect를 사용하는 것이 옳습니다.
📦 SSR에서 사용 가능한가?
결론부터 말하자면 불가능하다. 요소의 실제 크기를 계산하는 것과 관련된 모든 작업은 서버에서 간단하게 작동하지 않는다. 이러한 작업은 브라우저에서 일어나기 때문이다. 그러므로 useLayoutEffect를 서버에서 실행하는 것은 의미가 없다.
Next.js
- 기본적으로 server component에서는 훅을 사용할 수 없기에 애초에 성립이 되지 않는다.
대체 방안
- 애초에 useLayoutEffect를 사용하지 않고 렌더링이 완료된 이후에 화면에 컴포넌트를 그려내는 방식으로 바꿀 수도 있다.
const Component = () => {
const [shouldRender, setShouldRender] = useState(false);
useEffect(() => {
setShouldRender(true);
}, []);
if (!shouldRender) return <SomeNavigationSubstitude />;
return <Navigation />;
};
하지만 이러한 방식 또한 초기랜더링 시간이 늘어나는 것이기에 권장하지는 않는다.
- 혹은 상태를 이용하지 않고 조건부로 랜더링을 하려는 경우도 있을 것이다.
const Component = () => {
// SSR에서 window가 있는지 확인합니다.
if (typeof window === undefined) return <SomeNavigationSubstitude />;
return <Navigation />;
};
이러한 경우는 더욱 권장하지 않는다. 그 이유로서는 HTML과 첫번째 렌더링 결과가 일치해야하고 그렇지 않는다면 또 다른 문제를 발생시키기 때문이다.
📌 Reference
'CS > 프로그래밍' 카테고리의 다른 글
[Typescript] Generic과 forwardRef (0) | 2024.04.17 |
---|---|
DX (2) | 2024.04.03 |
Zustand란 무엇인가? (0) | 2024.03.23 |
MSA(마이크로서비스 아키텍처) (0) | 2024.03.23 |
디자인 패턴 - 팩토리 패턴 / 전략 패턴 / 옵저버 패턴 (1) | 2024.03.22 |