10.1 useLayoutEffect란?10.1.1 useLayoutEffect 개념10.2 useLayoutEffect를 사용하는 이유10.2.1 useLayoutEffect 사용 시 주의사항10.3 useEffect vs useLayoutEffect10.3.1 useEffect와 useLayoutEffect의 차이점10.3.2 (실습1) useLayoutEffect와 useEffect 비교10.3.3 (실습2) useLayoutEffect와 useEffect 비교
10.1 useLayoutEffect란?
useLayoutEffect는 앞서 살펴본 useEffect와 거의 비슷한 기능을 하는 Hook입니다. 이번 챕터에서는 두 가지 Hook을 어떻게 구분해서 써야 하는지를 위주로 살펴보겠습니다.
10.1.1 useLayoutEffect 개념
useLayoutEffect는 React 컴포넌트가 렌더(Render) 과정을 거친 뒤, 동기적(synchronous)으로 실행됩니다. 컴포넌트가 브라우저에 페인트(Paint) 되기 전에 실행되기 때문에 DOM에서 레이아웃을 읽고 동기적으로 리렌더링하는 경우에 사용하는 것이 적합합니다. useLayoutEffect를 적절하게 사용하면 사용자 편의를 개선할 수 있습니다.
10.2 useLayoutEffect를 사용하는 이유
일반적으로 state가 조건에 따라 첫 페인트 과정에서부터 보여져야 할 때 useLayoutEffect를 사용하면 원하는 결과를 얻을 수 있습니다. 이 경우 비슷한 동작을 하는 useEffect를 사용하게 되면 첫 페인팅에서 state가 원하는 값이 아닌 초깃값으로 렌더링 되고, 리렌더링 되면서 화면이 깜빡거리는 현상이 발생할 수 있습니다.
이러한 깜빡거리는 현상을 방지하고자 할 때 useLayoutEffect를 사용합니다. useEffect와 useLayoutEffect의 차이점은 10.3에서 자세히 살펴보겠습니다.
(참고) 일반적으로
useEffect
를 사용하는 경우는 아래와 같습니다.- 데이터를 비동기로 받아올 때 (fetch)
- event handler를 적용할 때
- state를 리셋할 때
10.2.1 useLayoutEffect 사용 시 주의사항
SSR(Server-Side Rendering) 환경에서 useLayoutEffect를 사용하면 자바스크립트가 모두 실행되기 전까지 useLayoutEffect는 서버 렌더러의 출력형식으로 인코딩할 수 없기 때문에 서버에서 아무 작업도 수행하지 못합니다. 따라서 의도했던 UI와 일치하지 않는 현상이 발생합니다. 이러한 문제를 사전에 방지하기 위해 '클라이언트'에서 렌더링하는 컴포넌트에서만 useLayoutEffect를 사용해야 합니다.
또한, useLayoutEffect는 동기적으로 실행되기 때문에 내부에 서버에서 불러오는 시간이 오래 걸리는 작업이 있거나 내부의 로직이 복잡할 경우 레이아웃이 브라우저에 보여지기까지 시간이 오래 걸린다는 단점이 있습니다. 상황에 따라 useLayoutEffect를 적절하게 사용해야하며 기본적으로 useEffect를 사용하는 것을 권장합니다.
서버 사이드 렌더링(SSR, Server Side Rendering)은
서버에서 완성된 html 파일을 만들어서 클라이언트에게 보여주는 방식입니다.
반대로 클라이언트 사이드 렌더링(CSR, Client Side Rendering)은 클라이언트가 서버에서 데이터를 받아 동적으로 html을 직접 만들어 보여주는 방식입니다.
10.3 useEffect vs useLayoutEffect
10.3.1 useEffect와 useLayoutEffect의 차이점
useLayoutEffect와 useEffect는 실행되는 순서에서 차이가 있습니다.
React는 React DOM 트리를 구성하기 위한 렌더와 실제 브라우저에 그려주는 페인트 과정을 거쳐 컴포넌트를 브라우저에 렌더링합니다. 아래는 useEffect 챕터에서 설명했던 컴포넌트의 생명주기 관련 이미지입니다. 컴포넌트의 업데이트 단계에서 렌더와 페인트 과정을 기준으로 두 가지 Hook의 실행 순서 차이를 확인할 수 있습니다.
.png?table=block&id=0f5e6601-20b3-455b-ba3c-8579f77685d4&cache=v2)
useEffect는 컴포넌트의 렌더와 페인트 과정이 모두 끝난 뒤에 실행됩니다. 따라서 useEffect 함수 내부에 DOM에 영향을 주는 코드가 있을 경우 리렌더링 되면서 화면의 깜빡임 현상이 발생할 수 있습니다.
반면에 useLayoutEffect는 컴포넌트가 브라우저에 페인트 되기 전에 실행됩니다. 즉, useLayoutEffect 내부의 코드는 사용자에게 보여지기 전에 실행되기 때문에 화면의 깜빡임 현상이 발생하지 않는 것 입니다.
useLayoutEffect
vs useEffect
실행 순서 비교useLayoutEffect
component
→ render
→ useLayoutEffect
→ paint
useEffect
component
→ render
→ paint
→ useEffect
아래의 예제 코드로 직접 확인해봅시다.
import { useEffect, useLayoutEffect } from 'react'; function App() { useEffect(() => { console.log('useEffect 실행!'); }, []); useLayoutEffect(() => { console.log('useLayoutEffect 실행!'); }, []); return ( <div>실행 순서를 확인해봅시다!</div> ); } export default App;
useEffect와 useLayoutEffect를 동시에 사용하면 실행 순서의 차이로 인해 나중에 작성한 useLayoutEffect가 먼저 콘솔에 찍히는 것을 확인할 수 있습니다.

따라서 useEffect보다 먼저 실행하고 싶은 코드가 있다면 useLayoutEffect를 사용할 수 있습니다.
10.3.2 (실습1) useLayoutEffect와 useEffect 비교
10.3.1절에서 useLayoutEffect와 useEffect의 실행 순서 차이에 대해 살펴보았습니다. 지금부터는 예제 코드를 통해 두 Hook이 어떻게 다르게 호출되어 브라우저에 페인트하는지 살펴보도록 하겠습니다. 아래는 렌더링 이후 useLayoutEffect와 useEffect가 호출되는 시점의 차이를 확인하는 예제 코드입니다.
import React, { useState, useLayoutEffect, useEffect} from "react"; function App() { const [number1, setNumber1] = useState(0); const [number2, setNumber2] = useState(0); useEffect(() => { setNumber1(parseInt(Math.random() * 100)); }, []); useLayoutEffect(() => { setNumber2(parseInt(Math.random() * 200)); }, []); console.log("useEffect : ", number1); console.log("useLayoutEffect : ", number2); return ( <div style={{ paddingLeft: '10px' }}> <h1>useEffect: {number1}</h1> <h1>useLayoutEffect: {number2}</h1> </div> ); }; export default App;
위 코드를 실행하면 이벤트가 발생하는 순서는 다음과 같습니다. 먼저 아래의 그림 10-3의 콘솔창에서 최초 렌더링이 일어나 number1와 number2의 상태값이 0으로 보여지는 것을 확인할 수 있습니다.

컴포넌트 렌더 과정 다음에 useLayoutEffect가 호출됩니다. 하지만, 브라우저가 변경 내역을 화면에 그리기 전에 호출됩니다. 따라서 동기적으로 useLayoutEffect가 실행이 끝나고 난 후 브라우저에 페인트하여 아래 그림 10-4 처럼 number2의 상태가 181로 바뀌고 useEffect는 아직 실행되지 않았기 때문에 0인 것을 확인할 수 있습니다.
.png?table=block&id=88f55044-8ce2-4583-a9c0-57e46444a4c1&cache=v2)
다음으로 useEffect가 호출되어 number1의 상태값은 3으로 바뀌고 아래 그림 10-5처럼 나타나게 됩니다.

이렇듯 대부분의 경우에서 useEffect로 원하는 작업을 수행하기에 충분하겠지만, 브라우저에 페인트 되기 전에 수행하기를 원하면 useLayoutEffect를 사용할 수 있습니다.
10.3.3 (실습2) useLayoutEffect와 useEffect 비교
앞서 언급한 바와 같이, useEffect와 useLayoutEffect는 실행 순서에 차이가 있을 뿐만 아니라, 비동기적으로 실행되는 useEffect와 달리 useLayoutEffect는 동기적으로 실행된다는 점에서 차이가 있습니다. 아래는 클릭 이벤트가 발생할 때마다 박스의 크기가 리렌더링되는 것을 확인하는 예제 코드와 실행 결과입니다.
import { useState, useEffect, useLayoutEffect } from 'react' function App() { const [effectValue, setEffectValue] = useState(100); const [layoutEffectValue, setLayoutEffectValue] = useState(100); useEffect(() => { for (let index = 0; index < 40000; index++) { for (let index2 = 0; index2 < 40000; index2++) { } } if (effectValue >= 250) { setEffectValue(200); } }, [effectValue]); useLayoutEffect(() => { for (let index = 0; index < 40000; index++) { for (let index2 = 0; index2 < 40000; index2++) { } } if (layoutEffectValue >= 250) { setLayoutEffectValue(200); } }, [layoutEffectValue]); return ( <div style={{ padding: '20px' }}> <div style={{ display: 'flex' }}> <div style={{ width: effectValue, height: effectValue, backgroundColor: 'blue' }}> </div> <div style={{ width: layoutEffectValue, height: layoutEffectValue, backgroundColor: 'aqua' }}> </div> </div> <button onClick={() => { setEffectValue(250); setLayoutEffectValue(250); }}> 커져랏! </button> <button onClick={() => { setEffectValue(150); setLayoutEffectValue(150); }}> 작아져랏! </button> </div> ) } export default App;

위의 코드에서는 effectValue, layoutEffectValue 2개의 state를 통해 각각 첫 번째, 두 번째 div 영역의 크기를 결정하고 있습니다. useEffect와 useLayoutEffect에서는 각각 effectValue, layoutEffectValue 2개의 state를 감시하고 있으며, state들의 크기가 250 이상으로 변경될 경우, 해당 state의 값을 setState를 통해 200으로 조절하는 역할을 하고 있습니다.
‘커져랏’ 버튼을 클릭할 경우, 해당 버튼의 클릭 이벤트에 등록된 함수가 실행되며, effectValue, layoutEffectValue 2개의 state의 값을 250으로 변경합니다. 그리고 state의 값이 변경되었기 때문에 렌더링 단계를 수행하게 됩니다.
이때 useEffect는 페인팅 단계가 마무리된 후, 비동기적으로 실행됩니다. 때문에 초기에 useEffect가 실행되기 전, 첫 번째 페인팅 단계에서는 그림 10-6의 2단계와 같이 첫 번째 div의 크기가 250인 상태로 화면에 그려집니다. 해당 과정이 끝난 후, useEffect가 실행되고, useEffect에서 감시하고 있는 effectValue의 값이 250 이상이므로 effectValue를 200으로 변경해줍니다. 이후 state의 값이 변경되었으므로 다시 렌더링 단계를 수행하고, 그림 10-6의 3단계와 같이 첫 번째 div의 크기가 200으로 변경되어 화면에 다시 페인팅 됩니다.
그렇지만 useLayoutEffect의 경우에는 페인팅 단계가 수행되기 전에, 동기적으로 수행되므로 해당 훅의 실행이 완료되기 전까지 페인팅 단계는 시작하지 않습니다. useLayoutEffect가 실행될 때, 감시하고 있던 layoutEffectValue의 값이 250 이상이므로 해당 값을 200으로 변경하고, 렌더링 단계가 다시 수행하게 됩니다. 이후 두번째 div의 크기가 200인 상태로 그림 10-6의 2단계와 같이 화면에 페인팅 되고, 페인팅 단계가 끝났으므로 앞의 useEffect가 실행될 수 있게 됩니다.
따라서 UI 엘리먼트의 모양을 화면에 표시하는데 잠깐의 깜빡임 현상이 존재한다면 useLayoutEffect를 사용하는 것을 고려해볼 수 있습니다.