리액트 레퍼런스 Hooks - useLayoutEffect

기운찬곰·2023년 10월 9일
post-thumbnail

원문 : https://react.dev/reference/react/useLayoutEffect

useLayoutEffect

❗️ useLayoutEffect를 사용하면 성능이 저하될 수 있습니다. 가능하면 useEffect를 선호합니다.

reference

useLayoutEffect는 브라우저가 화면을 repaints 하기 전에 발생하는 useEffect 버전입니다.

useLayoutEffect(setup, dependencies?)

브라우저가 화면을 repaints 전에 레이아웃 측정을 수행하려면 useLayoutEffect를 호출하세요.

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);
  }, []);
  // ...

Caveats

  • useLayoutEffect는 Hook이므로 컴포넌트의 최상위 수준이나 자체 Hook에서만 호출할 수 있습니다. 루프나 조건 내에서는 호출할 수 없습니다. 필요한 경우 구성 요소를 추출하고 Effect 를 이동하십시오.
  • Strict 모드가 켜져 있으면 React는 첫 번째 실제 설정 전에 추가 개발 전용 설정+정리 주기를 한 번 더 실행합니다. 이는 정리 논리가 설정 논리를 "미러링"하고 설정이 수행하는 모든 작업을 중지하거나 실행 취소하는지 확인하는 스트레스 테스트입니다. 이로 인해 문제가 발생하면 정리 기능을 구현하십시오.

Usage

Measuring layout before the browser repaints the screen

대부분의 컴포넌트는 무엇을 렌더링할지 결정하기 위해 화면에서의 위치와 크기를 알 필요가 없습니다. 일부 JSX만 반환합니다. 그런 다음 브라우저는 레이아웃(위치 및 크기)을 계산하고 화면을 다시 그립니다.

때로는 그것만으로는 충분하지 않습니다. 마우스를 올리면 일부 요소 옆에 tooltip(도구 설명)이 표시된다고 상상해 보세요. 공간이 충분하면 툴팁이 요소 위에 나타나야 하고, 맞지 않으면 아래에 나타나야 합니다. 올바른 최종 위치에 tooltip을 렌더링하려면 높이(즉, 상단에 맞는지 여부)를 알아야 합니다.

이렇게 하려면 두 번의 패스로 렌더링해야 합니다:

  1. tooltip을 어디에서나 렌더링합니다(위치가 잘못된 경우에도).
  2. 높이를 측정하고 tooltip을 배치할 위치를 결정합니다.
  3. tooltip을 올바른 위치에 다시 렌더링합니다.

이 모든 작업은 브라우저가 화면을 다시 그리기 전에 발생해야 합니다. 사용자가 tooltip이 움직이는 것을 보는 것을 원하지 않습니다. 브라우저가 화면을 다시 그리기 전에 레이아웃 측정을 수행하려면 useLayoutEffect를 호출하세요.

function Tooltip() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0); // You don't know real height yet

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height); // Re-render now that you know the real height
  }, []);

  // ...use tooltipHeight in the rendering logic below...
}

다음은 이 작업이 단계별로 수행되는 방법입니다:

  1. Tooltip은 초기 tooltipHeight=0으로 렌더링됩니다 (따라서 tooltip의 위치가 잘못될 수 있음).
  2. React는 이를 DOM에 배치하고 useLayoutEffect에서 코드를 실행합니다.
  3. useLayoutEffect는 tooltip content의 높이를 측정하고 즉시 다시 렌더링을 트리거합니다.
  4. Tooltip은 실제 tooltipHeight로 다시 렌더링됩니다(따라서 도구 설명이 올바르게 배치됩니다).
  5. React는 이를 DOM에서 업데이트하고 브라우저는 마침내 tooltip을 표시합니다.

아래 버튼 위로 마우스를 가져가서 툴팁이 맞는지 여부에 따라 위치가 어떻게 조정되는지 확인하세요.

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 컴포넌트가 두 번의 패스(첫 번째 0으로 초기화된 tooltipHeight와 그런 다음 실제 측정된 높이)로 렌더링되어야 하더라도 당신은 최종 결과만 볼 수 있습니다. 이것이 이 예제에서 useEffect 대신 useLayoutEffect가 필요한 이유입니다. 아래에서 자세한 차이점을 살펴보겠습니다.

React는 브라우저가 화면을 다시 그리기 전에 useLayoutEffect 내부의 코드와 내부에 예약된 모든 상태 업데이트가 처리되도록 보장합니다. 이를 통해 사용자가 첫 번째 추가 렌더링을 눈치채지 못한 채 tooltip을 렌더링하고, 측정하고, tooltip을 다시 렌더링할 수 있습니다. 즉, useLayoutEffect는 브라우저가 페인팅하는 것을 차단합니다.


다음은 동일한 예이지만 useLayoutEffect 대신 useEffect를 사용합니다. 속도가 느린 장치를 사용하는 경우 때때로 tooltip이 "깜빡거리고" 수정된 위치 이전에 초기 위치가 잠시 표시되는 것을 볼 수 있습니다. 버그를 더 쉽게 재현할 수 있도록 이 버전에서는 렌더링 중에 인위적인 지연을 추가했습니다. React는 useEffect 내에서 상태 업데이트를 처리하기 전에 브라우저가 화면을 그리도록 합니다. 결과적으로 tooltip이 깜박입니다.

import { useRef, useEffect, 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);

  // This artificially slows down rendering
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Do nothing for a bit...
  }

  useEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(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
  );
}

아래는 useEffect 내에서 상태 업데이트를 처리하기 전에 브라우저가 보여준 화면이다. 그리고 나서 useEffect 내용이 실행되면 깜빡이는 현상처럼 보이는거지.

💡 흠... 이제야 어떤 느낌인지 예상이 간다. useEffect는 초기 렌더링을 보여준 다음, 다시 리렌더링으로 보여주니까 깜빡이는 현상이 나타나는거고... useLayoutEffect는 초기 렌더링을 브라우저가 페인팅하는것을 차단하고 이후 실제 내부 코드가 실행된 상태를 보여주니까 깜빡이는게 없겠구나

두 번의 패스로 렌더링하고 브라우저를 차단하면 성능이 저하됩니다. 되도록이면 이 사용을 피하도록 합니다.


Troubleshooting

I’m getting an error: ”useLayoutEffect does nothing on the server”

useLayoutEffect의 목적은 컴포넌트가 렌더링을 위해 레이아웃 정보를 사용하도록 하는 것입니다.

  1. 초기 콘텐츠를 렌더링합니다.
  2. 브라우저가 화면을 다시 그리기 전에 레이아웃을 측정합니다.
  3. 읽은 레이아웃 정보를 사용하여 최종 콘텐츠를 렌더링합니다.

당신 또는 당신의 프레임워크가 서버 렌더링을 사용하는 경우 React 앱은 초기 렌더링을 위해 서버에서 HTML로 렌더링됩니다. 이를 통해 JavaScript 코드가 로드되기 전에 초기 HTML을 표시할 수 있습니다.

문제는 서버에 레이아웃 정보가 없다는 것입니다.

이전 예에서는 Tooltip 컴포넌트의 useLayoutEffect 호출을 통해 콘텐츠 높이에 따라 위치를 올바르게(콘텐츠 위 또는 아래) 지정할 수 있습니다. 초기 서버 HTML의 일부로 도구 설명을 렌더링하려고 시도한 경우 이를 확인하는 것이 불가능합니다. 서버에는 아직 레이아웃이 없습니다! 따라서 서버에서 렌더링하더라도 JavaScript가 로드되고 실행된 후 클라이언트에서 해당 위치가 "jump"됩니다.

일반적으로 레이아웃 정보에 의존하는 컴포넌트는 서버에서 렌더링할 필요가 없습니다. 예를 들어 초기 렌더링 중에 Tooltip을 표시하는 것은 의미가 없을 수 있습니다. 이는 클라이언트 상호 작용에 의해 트리거됩니다. (하긴... 그렇지)

하지만 이 문제가 발생하는 경우 몇 가지 다른 옵션이 있습니다.

  • useLayoutEffect를 useEffect로 바꾸세요. 이는 페인트를 차단하지 않고 초기 렌더링 결과를 표시해도 괜찮다는 것을 React에 알려줍니다 (원본 HTML은 Effect가 실행되기 전에 표시되기 때문입니다).
  • 또는 컴포넌트를 클라이언트 전용으로 표시하세요. 이는 React가 서버 렌더링 중에 가장 가까운 <Suspense> 경계까지 콘텐츠를 loading fallback(예: spinner 또는 glimmer)으로 바꾸도록 지시합니다.
  • 또는 hydration 후에만 useLayoutEffect를 사용하여 컴포넌트를 렌더링할 수 있습니다. false로 초기화된 boolean isMounted 상태를 유지하고 useEffect 호출 내에서 이를 true로 설정합니다. 그러면 렌더링 논리는 return isMounted ? <RealContent /> : <FallbackContent /> 가 될 수 있습니다. 서버에서 그리고 hydration 중에 사용자는 useLayoutEffect를 호출해서는 안 되는 FallbackContent를 볼 수 있습니다. 그런 다음 React는 이를 클라이언트에서만 실행되고 useLayoutEffect 호출을 포함할 수 있는 RealContent로 대체합니다.
  • 컴포넌트를 외부 데이터 저장소와 동기화하고 레이아웃 측정과 다른 이유로 useLayoutEffect를 사용하는 경우 대신 서버 렌더링을 지원하는 useSyncExternalStore를 고려하세요.

마치면서

매번 useEffect, useLayoutEffect가 헷갈렸는데 이제 좀 확실히 알거 같네요.

profile
부계정

0개의 댓글