useEffect는 많이 썼다만 useLayoutEffect는 대체 뭘까?
시니어 프론트엔드 개발자 테오님의 카카오톡 그룹 채팅방을 보던 도중, useEffect는 가끔 Paint 작업 이전에도 실행될 수 있느냐는 질문을 보았다. 처음에는 엥? useEffect는 항상 브라우저가 완전히 렌더링 된 이후에 실행되는 거 아닌가? 일부러 React 에서 약간의 timeout 까지 줘가며 Passive Effect를 실행시키는데, 이게 말이 되나 싶어 그렇지 않다는 답글을 달았다.
하지만 질문자 분께서 제공해주신 포스팅을 보니, 내가 알고 있던 상식이 뒤바뀌는 결과를 낳게 되어 이 번뜩이는 지식을 재빨리 글로서 정리하고자 한다. 우리가 알고 있었던 useEffect의 당연한 공식이 어쩌면 가끔은 비정상적으로 동작할 수 있다는 유익한 정보를 여러분들께서도 알기 바란다.
useEffect
는 일반적으로 React의 Commit Phase를 마친 후, 브라우저가 렌더링 된 후에 실행된다.useEffect
훅은 대부분의 경우 브라우저의 Layout과 Paint 작업을 마친 이후 실행된다.useEffect
훅 내부에 DOM을 건드리는 요소가 존재할 경우 리렌더링을 발생시켜 화면의 깜빡임을 유발시킨다.useLayoutEffect
훅은 컴포넌트가 commit Phase에서 DOM이 수정된 이후에 동기적으로 실행된다.useLayoutEffect
의 작업이 완료되면, React는 제어권을 브라우저에게 위임하고 Paint 작업을 진행하게끔 한다.useLayoutEffect
내부에 DOM을 건드리는 요소가 있더라도 Paint 작업 이전에 모두 완료되므로 깜빡임이 없다.useLayoutEffect
사용이 바람직하다.useEffect
사용이 좋다.useEffect
를 거의 사용한다. useLayoutEffect
는 사용을 자주 하지 않는다.useEffect
훅이 Paint 작업 이후에 실행되도록 보장한다.useLayoutEffect
에서 리렌더링이 발생할 경우, useEffect
의 작업인 Passive Effect가 Paint 작업 이전에 실행된다.const ResponsiveInput = ({ onClear, ...props }) => {
const el = useRef();
const [w, setW] = useState(0);
// measure 함수에서 setState 함수가 호출된다. state update
const measure = () => setW(el.current.offsetWidth);
// Paint 작업 이전에 state가 업데이트 되었으므로 re-render 발생.
useLayoutEffect(() => measure(), []);
useEffect(() => {
window.addEventListener("resize", measure);
return () => window.removeEventListener("resize", measure);
}, []);
return (
<label>
<input {...props} ref={el} />
{w > 200 && <button onClick={onClear}>clear</button>}
</label>
);
};
useLayoutEffect
훅에서 setState 함수를 호출시켜 리렌더링을 발생시킨다.useLayoutEffect
훅을 호출하며 state를 업데이트 한다.useEffect
훅을 호출한다.useLayoutEffect
훅을 재호출한다.useEffect
훅이 호출된다.React will always flush a previous render’s effects before starting a new update.
useEffect
훅이 관할하는 Passive Effect의 경우에도 예외없이 실행되고, 이는 Paint 작업 이전에 useEffect
가 호출되게끔 한다.useEffect
에 의존하지 말자.useLayoutEffect
훅에 리렌더링을 유발시키는 요소를 넣는 행위는 이상적인 케이스가 아니기에 (de-opt) 되도록이면 사용을 피하자.const clearRef = useRef();
const measure = () => {
// React의 state를 사용하는 대신, DOM을 useLayoutEffect 에서 직접 핸들링.
clearRef.current.display = el.current.offsetWidth > 200 ? null : none;
};
useLayoutEffect(() => measure(), []);
useEffect(() => {
window.addEventListener("resize", measure);
return () => window.removeEventListener("resize", measure);
}, []);
return (
<label>
<input {...props} ref={el} />
<button ref={clearRef} onClick={onClear}>
clear
</button>
</label>
);
React 의 컴포넌트 외부에서 작동되는 모든 작업이 Side Effect 이다.
일반적인 React 의 Side Effect 로 취급되는 요소는 아래와 같다.
React에서는 useEffect
훅을 통해 컴포넌트 내부에서 이러한 Side Effect를 처리하도록 한다.
왜냐하면 Side Effect는 렌더링 과정에서 영향을 주면 안되므로, 렌더링 이후에 실행되는 useEffect
가 적합하기 때문이다.