함정!useLayoutEffect(setup, dependencies?)useLayoutEffect의 매개변수useLayoutEffct 반환값useLayoutEffect 사용시 주의사항useLayoutEffect 사용법1. 브라우저에서 화면을 다시 그리기 전 레이아웃 측정하기문제해결1. 오류가 발생합니다: “useLayoutEffect가 서버에서 아무것도 수행하지 않아요ˮ
useLayoutEffect
는 브라우저가 화면을 다시 채우기 전에 실행되는 버전의 useEffect
이다.함정!
useLayoutEffect
는 성능을 저하시킬 수 있다. 가급적이면 useEffect
를 사용하세요useLayoutEffect(setup, dependencies?)
브라우저가 화면을 다시 그리기 전에
useLayoutEffect
를 호출하여 레이아웃(layout)을 측정한다.import { useState, useRef, useLayoutEffect } from 'react'; function Tooltip() { const ref = useRef(null); const [tooltipHeight, setTooltipHeight] = useState(0); useLayoutEffect(() => { const { height } = ref.current.getBoundingClientRect(); setTooltipHeight(height); }, []); // ...
useLayoutEffect의 매개변수
setup
: Effect의 로직이 포함된 함수이다. 셋업 함수는 선택적으로 cleanup 함수를 반환할 수 있다. 컴포넌트가 DOM에 추가되기 전에 React는 setup 함수를 실행한다. 변경된 의존성으로 다시 렌더링할 때마다 React는 (cleanup 함수를 정의한 경우) 먼저 이전 값으로 cleanup 함수를 실행한 다음, 새 값으로 setup 함수를 실행한다. 컴포넌트가 DOM에서 제거되기 전에 React는 cleanup 함수를 한 번 더 실행한다.dependencies
(optional 매개변수): setup
코드 내에서 참조된 모든 반응형 값의 목록이다. 반응형 값에는 props, state, 컴포넌트 본문 내부에서 직접 선언된 모든 변수와 함수가 포함됩니다. 린터가 React용으로 설정된 경우, 모든 반응형 값이 의존성으로 올바르게 지정되었는지 확인한다. 의존성 목록은 일정한 수의 항목을 가져야 하며 [dep1, dep2, dep3]
와 같이 인라인으로 작성해야 한다. React는 Object.is
비교를 사용하여 각 의존성을 이전 값과 비교한다. 이 인수를 생략하면 컴포넌트를 다시 렌더링할 때마다 Effect가 다시 실행된다.useLayoutEffct 반환값
useLayoutEffect
는 undefined
를 반환한다.useLayoutEffect 사용시 주의사항
1)
useLayoutEffect
는 훅이므로 컴포넌트의 최상위 레벨 또는 자체 훅에서만 호출할 수 있다. 반복문이나 조건문 내부에서는 호출할 수 없다. 필요하다면 컴포넌트를 추출하고 Effect를 그곳으로 이동해라2) Strict Mode가 켜져 있으면 React는 첫 번째 실제 setup 전에 개발 전용 setup+cleanup 사이클을 한 번 더 실행한다. 이는 cleanup 로직이 setup 로직을 “미러링”하고 설정이 수행 중인 모든 작업을 중지하거나 취소하는지 확인하는 스트레스 테스트이다. 문제가 발생하면 클린업 함수를 구현해라
3) 의존성 중 일부가 컴포넌트 내부에 정의된 객체 또는 함수인 경우, Effect가 필요 이상으로 자주 다시 실행될 위험이 있다.
const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); const options = { // 🚩 re-render 될 떄마다 생성되는 객체 - 함수도 마찬가지이다. serverUrl: serverUrl, roomId: roomId }; useEffect(() => { const connection = createConnection(options); // It's used inside the Effect connection.connect(); return () => connection.disconnect(); }, [options]); // 🚩 As a result, these dependencies are always different on a re-render // ...
이 문제를 해결하려면 불필요한 객체 및 함수 의존성을 제거해라 또한 Effect 외부에서 state 업데이트와 비반응형 로직을 추출할 수도 있다.
// 비반응형 로직이라면 import { useState, useEffect } from 'react'; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(c => c + 1); // ✅ Pass a state updater // 직접 값을 주는 것이 아니라 state 업데이터를 전달해서 의존성을 없애는 것! }, 1000); return () => clearInterval(intervalId); }, []); // ✅ Now count is not a dependency return <h1>{count}</h1>; }
4) LayoutEffect는 클라이언트에서만 실행된다. 서버 렌더링 중에는 실행되지 않는다.
5)
useLayoutEffect
내부의 코드와 여기에서 예약된 모든 state 업데이트는 브라우저가 화면을 다시 그리는 것을 차단한다. 과도하게 사용하면 앱이 느려진다. 가급적이면 useEffect
를 사용해라useLayoutEffect 사용법
1. 브라우저에서 화면을 다시 그리기 전 레이아웃 측정하기
대부분의 컴포넌트는 무엇을 렌더링할지 결정하기 위해 화면에서의 위치와 크기를 알 필요가 없다. 일부 JSX만 반환하기 때문이다. 그런 다음 브라우저는 해당 컴포넌트의 레이아웃(위치 및 크기)을 계산하고 화면을 다시 그린다.
때론 그것만으로는 충분하지 않을 수 있다. 마우스오버 시 요소 옆에 툴팁을 표시하는 것을 상상해봐라. 공간이 충분하면 툴팁이 요소 위에 표시되어야 하지만, 공간이 충분하지 않으면 아래에 표시되어야 한다. 툴팁을 올바른 최종 위치에 렌더링하려면 툴팁의 높이(즉,상단에 표시하기에 충분한지 여부)를 알아야 한다.
이렇게 하려면 두 번의 패스로 렌더링해야 한다
- 툴팁을 원하는 위치에 렌더링한다(위치가 잘못된 경우에도).
- 높이를 측정하고 툴팁을 배치할 위치를 결정한다.
- 올바른 위치에 툴팁을 다시 렌더링한다.
이 모든 작업은 브라우저가 화면을 다시 그리기 전에 이루어져야 한다. 사용자가 툴팁이 움직이는 것을 보지 않기를 원한다. 브라우저가 화면을 다시 그리기 전에
useLayoutEffect
를 호출하여 레이아웃 측정을 수행한다:function Tooltip() { const ref = useRef(null); const [tooltipHeight, setTooltipHeight] = useState(0); // You don't know real height yet // 아직 실제 height 값을 모릅니다. useLayoutEffect(() => { const { height } = ref.current.getBoundingClientRect(); setTooltipHeight(height); // Re-render now that you know the real height // 실제 높이를 알았으니 이제 리렌더링 합니다. }, []); // ...use tooltipHeight in the rendering logic below... // ...아래에 작성될 렌더링 로직에 tooltipHeight를 사용합니다... }
1.
Tooltip
은 초기 tooltipHeight = 0
으로 렌더링된다(따라서 툴팁의 위치가 잘못 지정될 수 있음).- React는 이를 DOM에 배치하고
useLayoutEffect
에서 코드를 실행한다.
useLayoutEffect
는 툴팁 콘텐츠의 높이를 측정하고 즉시 다시 렌더링을 촉발한다.
Tooltip
이 실제tooltipHeight
로 다시 렌더링된다(따라서 툴팁이 올바르게 배치된다).
- React가 DOM에서 이를 업데이트하면 브라우저에 툴팁이 최종적으로 표시된다.
아래 버튼 위로 마우스를 가져가면 툴팁이 맞는지 여부에 따라 툴팁의 위치가 어떻게 조정되는지 확인할 수 있다:
import { useRef, useLayoutEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import TooltipContainer from './TooltipContainer.js'; export default function Tooltip({ children, targetRect }) { const ref = useRef(null); const [tooltipHeight, setTooltipHeight] = useState(0); useLayoutEffect(() => { const { height } = ref.current.getBoundingClientRect(); setTooltipHeight(height); console.log('Measured tooltip height: ' + height); }, []); let tooltipX = 0; let tooltipY = 0; if (targetRect !== null) { tooltipX = targetRect.left; tooltipY = targetRect.top - tooltipHeight; if (tooltipY < 0) { // It doesn't fit above, so place below. tooltipY = targetRect.bottom; } } return createPortal( <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}> {children} </TooltipContainer>, document.body ); }
Tooltip
컴포넌트가 두 번의 패스로 렌더링되어야 하지만(먼저 tooltipHeight
를 0으로 초기화한 다음 실제 측정된 높이로), 최종 결과만 볼 수 있다는 점에 유의해라. 이 예제에서 useEffect
대신 useLayoutEffect
가 필요한 이유이다. 아래에서 차이점을 자세히 살펴보겠다.메모!
두 번의 패스로 렌더링하고 브라우저를 차단하면 성능이 저하된다. 가능하면 피하세요.
문제해결
1. 오류가 발생합니다: “useLayoutEffect
가 서버에서 아무것도 수행하지 않아요ˮ
useLayoutEffect
의 목적은 컴포넌트가 렌더링에 레이아웃 정보를 사용하도록 하는 것이다:- 초기 콘텐츠를 렌더링한다.
- 브라우저가 화면을 다시 칠하기 전에 레이아웃을 측정한다.
- 읽은 레이아웃 정보를 사용하여 최종 콘텐츠를 렌더링한다.
- 사용자 또는 프레임워크가 서버 렌더링을 사용하는 경우, React 앱은 초기 렌더링을 위해 서버의 HTML로 렌더링된다. 이를 통해 JavaScript 코드가 로드되기 전에 초기 HTML을 표시할 수 있다. 문제는 서버에는 레이아웃 정보가 없다는 것이다.
이전 예시에서
Tooltip
컴포넌트의 useLayoutEffect
호출은 콘텐츠 높이에 따라 콘텐츠 위 또는 아래에 올바르게 배치되도록 한다. 초기 서버 HTML의 일부로 Tooltip
을 렌더링하려고 하면 이를 확인할 수 없다. 서버에는 아직 레이아웃이 없다! 따라서 서버에서 렌더링하더라도 JavaScript가 로드되고 실행된 후 클라이언트에서 그 위치가 “점프”된다. 일반적으로 레이아웃 정보에 의존하는 컴포넌트는 서버에서 렌더링할 필요가 없다.
예를 들어, 초기 렌더링 중에
Tooltip
을 표시하는 것은 의미가 없을 수 있습니다. 이는 클라이언트 상호작용에 의해 촉발된다.하지만 이 문제가 발생하는 경우 몇 가지 다른 옵션이 있다
useLayoutEffect
를useEffect
로 바꿔라. 이렇게 하면 React가 페인트를 막지 않고 초기 렌더링 결과를 표시해도 괜찮다고 알려준다(Effect가 실행되기 전에 원래 HTML이 보이게 되므로).
- 컴포넌트를 클라이언트 전용으로 표시해라. 이렇게 하면 서버 렌더링 중에 가장 가까운
<Suspense>
경계까지의 콘텐츠를 로딩 폴백(예: spinner 나 glimmer)으로 대체하도록 React에 지시한.
- hydration 후에만
useLayoutEffect
를 사용하여 컴포넌트를 렌더링할 수 있다.false
로 초기화된 불리언isMounted
state를 유지하고,useEffect
호출 내에서 이를true
로 설정한다. 그러면 렌더링 로직은 다음과 같을 수 있습니다:return isMounted ? <RealContent /> : <FallbackContent />
서버에서 hydration이 진행되는 동안 사용자는useLayoutEffect
를 호출하지 않는FallbackContent
를 보게 된다. 그러면 React는 클라이언트에서만 실행되고useLayoutEffect
호출하는RealContent
로 대체한다.
- 컴포넌트를 외부 데이터 저장소와 동기화하고 레이아웃 측정이 아닌 다른 이유로
useLayoutEffect
에 의존하는 경우, 대신 서버 렌더링을 지원하는useSyncExternalStore
를 고려해라.