Menu

React useLayoutEffect: 브라우저가 그리기 전에 측정하기

useLayoutEffect는 useEffect처럼 동작하지만, React가 DOM을 업데이트한 직후 브라우저가 그리기 전에 실행됩니다. 눈에 보이는 깜빡임 없이 요소를 측정하고 레이아웃을 조정할 때 쓰고, 그 밖의 모든 것에는 useEffect를 쓰세요.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

useLayoutEffect는 더 일찍 실행되는 useEffect입니다. React가 DOM을 업데이트한 직후, 브라우저가 새 프레임을 그리기 전에 실행됩니다. 그 안의 코드는 요소를 측정하고 레이아웃을 바꿀 수 있으며, 사용자는 최종 결과만 보게 됩니다. 실행되는 동안 그리기를 막으므로, 그 타이밍이 필요할 때만 사용하세요.

이펙트는 effect, layout effect 순서로 선언되어 있지만, 콘솔에는 렌더링마다 layout effect가 먼저 출력됩니다. 두 훅 사이에서는 선언 순서가 아니라 타이밍이 중요합니다.

문법

시그니처는 useEffect와 똑같습니다. setup 함수, 선택적인 클린업, 의존성 배열입니다.

useLayoutEffect(() => {
    // runs after the DOM update, before paint
    return () => {
        // cleanup, before the next run and on unmount
    };
}, [dependencies]);

useEffect에서 배운 의존성과 클린업에 관한 모든 내용이 그대로 적용됩니다. 유일한 차이는 React가 언제 호출하는지입니다.

각각은 언제 실행될까

렌더링 후 React는 DOM을 업데이트하고 정해진 순서로 세 가지 일을 합니다.

  1. 모든 레이아웃 이펙트의 클린업과 레이아웃 이펙트를 동기적으로 실행합니다.
  2. 브라우저가 그리게 합니다. 레이아웃 이펙트가 state를 설정했다면, React는 여전히 그리기 전에 먼저 다시 렌더링합니다.
  3. 일반 이펙트를 실행합니다. 보통 그린 뒤입니다.

트리 전체에서 모든 레이아웃 이펙트는 어떤 일반 이펙트보다 먼저 실행되고, 부모는 자식이 제자리에 놓여야 끝날 수 있으므로 자식이 부모보다 먼저 실행됩니다.

콘솔에는 두 레이아웃 이펙트가 먼저 Child, Parent 순으로 나오고, 그다음 두 일반 이펙트가 같은 순서로 나옵니다. key={n} 덕분에 클릭할 때마다 새 Parent가 마운트되므로 매번 같은 네 줄이 나옵니다. 각 이펙트에 로그를 남기는 클린업을 추가하고, 이전 것들이 새 것보다 먼저 실행되는 것을 지켜보세요.

깜빡임 문제

버튼 위에 나타나야 하지만 위에 공간이 없으면 아래로 가는 툴팁을 생각해 보세요. 툴팁이 DOM에 들어가서 높이를 측정할 수 있기 전까지는 공간이 있는지 알 수 없습니다. 그래서 렌더링이 두 번에 걸쳐 일어납니다.

  1. 측정할 수 있도록 툴팁을 어딘가에 렌더링합니다.
  2. 측정하고, 실제 위치를 계산하고, state를 설정하고, 다시 렌더링합니다.

useEffect를 쓰면 브라우저가 두 단계 사이에 그릴 수 있고, 사용자는 툴팁이 첫 번째 위치에서 두 번째 위치로 튀는 것을 봅니다. useLayoutEffect를 쓰면 두 번째 단계가 그리기 전에 일어나므로 첫 번째 위치는 절대 보이지 않습니다.

위쪽 버튼에 마우스를 올려 보세요. 위에 공간이 없으므로 툴팁이 아래에 나타납니다. 아래쪽 버튼에 올리면 위에 나타납니다. 콘솔은 측정 결과 어느 쪽을 골랐는지 알려 줍니다. useLayoutEffect를 useEffect로 바꾸고 다시 마우스를 올려 보세요. 이제 툴팁이 이동하기 전에 한 프레임 동안 상자 맨 위에서 번쩍일 수 있습니다. 빠른 기기에서는 너무 빨라서 알아차리지 못하는 경우가 많은데, 바로 그래서 이 버그가 배포된 뒤 느린 휴대폰에서 신고됩니다.

offsetHeight, offsetTop, getBoundingClientRect()는 모두 브라우저가 레이아웃을 계산하도록 강제하므로, 레이아웃 이펙트에서 읽으면 React가 방금 쓴 DOM의 실제 값을 얻습니다.

요소 측정하기

렌더링이 의존하는 모든 크기에 같은 패턴이 적용됩니다. 레이아웃 이펙트에서 ref로 읽고, state에 저장하고, 그 값으로 렌더링합니다.

버튼을 클릭하면 너비 줄이 텍스트와 같은 프레임에 업데이트됩니다. 두 번째 렌더링이 그리기 전에 일어나므로, 페이지에 새 텍스트와 이전 너비가 함께 보이는 일은 절대 없습니다. 의존성 배열에서 [text]를 지우면 측정이 자기 setWidth가 일으킨 렌더링을 포함해 렌더링마다 실행됩니다. 거기서 멈추는 것은 너비가 더 이상 바뀌지 않기 때문일 뿐입니다.

useEffect를 우선하세요

레이아웃 이펙트는 반환될 때까지, 그리고 state를 설정했다면 추가 렌더링이 끝날 때까지 그리기를 붙잡아 둡니다. 느린 레이아웃 이펙트는 페이지의 모든 업데이트를 느리게 느끼게 합니다. 다음 조건이 모두 참일 때만 useLayoutEffect를 사용하세요.

  • 이펙트가 레이아웃(크기, 위치, 스크롤)을 읽습니다.
  • 그 값에 따라 렌더링 결과를 바꿉니다.
  • 중간 상태가 눈에 보이고 잘못되어 보입니다.

그 밖의 모든 것(데이터 가져오기, 구독, 타이머, 로깅, 분석)은 useEffect에 속합니다. 간단한 규칙은 이렇습니다. useEffect로 시작하고, 깜빡임이 보일 때만 바꾸세요.

useEffect는 보통 그린 뒤에 실행되지만 항상 그렇지는 않습니다. 클릭이나 키 입력 같은 개별적인 사용자 동작에서 업데이트가 일어나면, React는 브라우저가 그리기 전에 이펙트를 실행할 수도 있습니다. 깜빡임이 어떤 상황에서는 나타나고 다른 상황에서는 나타나지 않는 이유 중 하나입니다.

서버 렌더링

이펙트는 서버 렌더링 중에 절대 실행되지 않으며, useLayoutEffect도 마찬가지입니다. 서버는 첫 렌더링의 HTML을 보내고, 레이아웃 이펙트는 브라우저에서 하이드레이션이 끝난 뒤에만 실행됩니다. React 18 이하에서는 그런 컴포넌트를 서버에서 렌더링하면 "useLayoutEffect does nothing on the server"라는 경고가 나왔지만, React 19는 그 경고를 없앴습니다.

측정 없이는 서버에서 렌더링한 첫 프레임이 잘못되어 보인다면, 흔한 해결 방법이 두 가지 있습니다. 중립적인 대체 UI를 렌더링하거나(예: 측정될 때까지 툴팁 숨기기), 컴포넌트를 클라이언트에서만 렌더링하는 것입니다.

function ClientOnly({ children }) {
    const [mounted, setMounted] = useState(false);
    useEffect(() => setMounted(true), []);
    return mounted ? children : null;
}

useInsertionEffect

세 번째 이펙트 훅인 useInsertionEffect도 있으며, 어떤 레이아웃 이펙트보다도 먼저 실행됩니다. 런타임에 <style> 태그를 생성하는 CSS-in-JS 라이브러리를 위해 존재합니다. 그 시점에 스타일을 삽입하면 요소를 측정하는 레이아웃 이펙트가 이미 스타일이 적용된 요소를 보게 됩니다. state를 업데이트할 수 없고 ref도 아직 연결되지 않으므로 애플리케이션 코드에서는 쓸모가 없습니다. 스타일링 라이브러리를 작성하는 것이 아니라면 필요하지 않습니다.

자주 묻는 질문

useEffect와 useLayoutEffect의 차이는 무엇인가요?

타이밍입니다. useLayoutEffect는 React가 DOM을 바꾼 뒤 브라우저가 그리기 전에 동기적으로 실행되므로, 그 안의 state 업데이트는 사용자가 무언가를 보기 전에 적용됩니다. useEffect는 보통 그린 뒤에 실행됩니다. API와 의존성 배열은 같습니다.

useLayoutEffect는 언제 써야 하나요?

이펙트가 레이아웃(크기나 위치)을 읽고 그에 따라 렌더링 결과를 즉시 바꿀 때입니다. 툴팁을 버튼 위나 아래에 배치하는 경우가 그렇습니다. 이것 없이는 사용자가 처음의 잘못된 위치를 한 프레임 동안 볼 수 있습니다.

useLayoutEffect는 성능에 나쁜가요?

끝날 때까지, 그리고 그것이 일으킨 리렌더링이 끝날 때까지 브라우저가 그리지 못하게 막습니다. 느린 레이아웃 이펙트는 페이지 전체를 느리게 느끼게 하므로, 눈에 보이는 깜빡임을 피해야 하는 경우가 아니라면 useEffect를 쓰세요.

useLayoutEffect는 서버에서 실행되나요?

아니요. 두 이펙트 모두 서버 렌더링 중에는 실행되지 않습니다. 예전 React 버전은 useLayoutEffect를 가진 컴포넌트를 서버에서 렌더링할 때 경고를 출력했습니다. React 19는 더 이상 그러지 않지만, 이펙트는 여전히 브라우저에서만 실행됩니다.

Coddy 프로그래밍 언어 일러스트

Coddy로 코딩 배우기

시작하기