React Hook-flow 이해하기
useEffect 완벽 가이드(A Complete Guide to useEffect)를 읽으며 모든 렌더링은 고유의 이펙트를 가진다는 것을 알 수 있었습니다. 그리고 그 이펙트는 React가 기억해놨다가 DOM 업데이트 후에 실행한다는 것을 알게되었습니다.
useEffect에 대해 어느정도 이해했다고 생각했지만 useEffect sometimes fires before paint라는 글의 제목을 보고 혼란에 빠졌고 이를 해결하기 위해 위의 글을 공부해보기로 했습니다.
useLayoutEffect
"Updating state in
useLayoutEffect
makes everyuseEffect
from the same render run before paint"
두괄식으로 useLayoutEffect
가 useEffect
를 DOM의 페인트 이전에 실행시키게 한다고 이야기합니다. 하지만 저는 지금까지 useLayoutEffect
를 사용해본적이 없어 이를 먼저 알아보려고 합니다.
공식문서
공식문서에서는 useLayoutEffect
를 다음과 같이 소개합니다.
이 함수의 시그니처는
useEffect
와 동일하긴 한데, 모든 DOM 변경 후에 동기적으로 발생합니다. 이것은 DOM에서 레이아웃을 읽고 동기적으로 리렌더링하는 경우에 사용하세요.
useLayoutEffect(() => {
effect
return () => {
cleanup
};
}, [input])
위와 같이 시그니쳐는 useEffect
와 동일합니다. useLayoutEffect
의 첫 번째 인수로 Effect를 전달하고 정리(Clean-up)가 필요한 경우 return
을 통해 할 수 있습니다. 그리고 두 번째 인수로 배열을 받아 특정 값들이 리렌더링시 변경되지 않는다면, effect를 실행하지 않도록 합니다. 시그니쳐만 봐서는 useEffect
와 동일해보이지만 공식문서의 '모든 DOM 변경 후에 동기적으로 발생합니다.'를 이해하면 useEffect
와의 차이를 알 수 있습니다.
useEffect와의 차이
useEffect
는 Effect를 DOM의 레이아웃 배치와 페인트가 끝난 후(DOM 업데이트 후) 호출합니다. 따라서 상태값이 Effect에 의존한 경우, 사용자가 불편을 겪을 수 있습니다.
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("이름");
useEffect(() => {
setCount(1);
setName("삼바");
}, []);
return (
<div className="App">
<h2>
{name} clicked {count} times
</h2>
</div>
);
}
위의 예제를 실행했을 경우, 다음 순서대로 동작합니다.
useState
의 초기값을 가지고이름 clicked 0 times
로 DOM 업데이트- Effect 를 호출
- Effect 내에서 State 값이 바뀌었으므로 재렌더링
삼바 clicked 1 times
로 DOM 업데이트
위의 예제처럼 상태값 변화에따라 사용자에게 의도하지 않은 컨텐츠 혹은 UI를 보여줄 수 있게됩니다. useLayoutEffect
는 이러한 문제를 해결해줍니다.
useLayoutEffect(() => {
setCount(1);
setName("삼바");
}, []);
useEffect
를 useLayoutEffect
로 바꾼 후, 예제를 실행해보면 이름 clicked 0 times
가 더이상 보이지 않습니다.
Hook-flow
hook-flow 프로젝트에 보면 Flow Diagram을 통해 useEffect
와 useLayoutEffect
의 Effect 가 실행되는 순서를 보실 수 있습니다.
useEffect somtimes fire before paint
useLayoutEffect
에 대해 알아봤으니 이제 useEffect
의 Effect가 DOM 페인트가 되기 전에 실행되는 경우를 알아봅시다. 이해를 돕기위해 이 단락에서 useLayoutEffect
의 Effect는 "LayoutEffect" useEffect
의 Effect는 "Effect"라고 부르겠습니다.
const ResponsiveInput = ({ onClear, ...props }) => {
const el = useRef();
const [w, setW] = useState(0);
const measure = () => setW(el.current.offsetWidth);
useLayoutEffect(() => measure(), []);
useEffect(() => {
// don't take this too seriously, say it's a ResizeObserver
window.addEventListener("resize", measure);
return () => window.removeEventListener("resize", measure);
}, []);
return (
<label>
<input {...props} ref={el} />
{w > 200 && <button onClick={onClear}>clear</button>}
</label>
);
};
위의 예제를 바탕으로 "Effect"가 DOM이 페인트되기 전에 실행되는 경우의 동작을 순서대로 따라가봅시다.
- React update 1: render virtual DOM, schedule effects, update DOM
- "LayoutEffect" 실행
- "LayoutEffect" 내에서 state를 변경하는 경우(
setW
)가 있어 재렌더링 - "Effect" 실행
- React update 2
- React update 2에서의 "LayoutEffect" 실행
- React releases control, browser paints the new DOM
- React update 2에서의 "Effect"실행
위의 그림을 보면 "Effect" 가 paint되기 전에 실행되는 경우를 잘 이해하실 수 있습니다.
두 번째 라인을 보면 "LayoutEffect"내에 state 변경이 있어 re-render가 일어나는 경우, "Effect"가 언제 일어날 것인지에 대해 물음을 던지고 있고, 세번째 줄을 보면 이 질문에 대한 답을 볼 수 있습니다.
"Effect"가 re-render가 일어나기 전에 실행되므로 결과적으로 paint 전에 실행됩니다.
마무리
useLayoutEffect
를 알아보고 이를 토대로 "useEffect somtimes fire before paint"를 이해할 수 있었습니다.
useLayoutEffect
는 useEffect
와 시그니처가 같지만, DOM을 레이아웃에 배치한 후 동기적으로 Effect를 실행합니다. 이는 useEffect
의 Effect에서 상태값을 바꾸어 사용자에게 불편함을 주는 것을 해소해주었습니다.
useEffect
의 Effect는 DOM이 업데이트된 이후에 실행되곤하지만, 이는 항상 보장되지는 않습니다. 그리고 useLayoutEffect
내에서 상태를 바꾸는 행위는 앱의 퍼포먼스에 부정적인 영향을 끼칠 수 있습니다. 따라서 useEffect
와 useLayoutEffect
를 사용할때 보다 신중해야합니다.
참고자료
- https://ko.reactjs.org/docs/hooks-reference.html#uselayouteffect
- useLayoutEffect 훅에 대하여: https://merrily-code.tistory.com/46
- useEffect somtimes fire before paint: https://thoughtspile.github.io/2021/11/15/unintentional-layout-effect/