[2023.06.17] TIL 39일차

김선민·2024년 6월 17일

useLayoutEffect란?

  • useLayoutEffect는 브라우저가 화면을 다시 채우기 전에 실행되는 useEffect 한 버전이다

useLayoutEffect 기본 형태

useLayoutEffect(setup, dependencies?)

  • setup : Effect의 주요 로직을 포함 / 컴포넌트가 DOM에 추가되거나, 의존성이 변경될 때 실행
  • dependencies : 배열 형태이며, 배열 안에는 검사하고자 하는 특정 값 or 빈 배열
  • 브라우저가 화면을 다시 그리기 전에 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);
      }, []);
      // ...

매개변수

  1. setup : Effect의 로직이 포함된 함수

    • 동작
      • 컴포넌트가 DOM에 추가되기 전에 실행됩니다.
      • 의존성이 변경될 때마다 다시 실행됩니다.
      • 선택적으로 클린업 함수를 반환할 수 있습니다.
    • cleanup 함수
      • 새 setup 함수가 실행되기 전에 이전 setup 함수의 클린업 함수가 실행
      • 컴포넌트가 DOM에서 제거되기 전에 실행됩니다
  2. optional deps : setup 코드 내에서 참조된 모든 반응형 값의 목록

    • props, state, 컴포넌트 내부에서 선언된 변수와 함수
    • 일정한 수의 항목을 가져야 하며, [dep1, dep2, dep3] 형식으로 인라인 작성해야 함
    • React는 Object.is를 사용하여 각 의존성을 이전 값과 비교
    • 이 인수를 생략하면 컴포넌트가 다시 렌더링될 때마다 Effect가 다시 실행

반환값

  • useLayoutEffect는 undefined를 반환한다.

주의사항

  1. 최상위 레벨에서 호출
    • useLayoutEffect는 훅이므로 컴포넌트의 최상위 레벨 또는 자체 훅에서만 호출 가능
    • 반복문이나 조건문 내부에서는 호출할 수 없다.
      • 필요하다면 컴포넌트를 추출하고 Effect를 그곳으로 이동
  1. Strict Mode에서의 동작

    • Strict Mode가 켜져 있으면 첫 번째 실제 셋업 전에 추가로 개발 전용 셋업+클린업 사이클이 실행
      • 이는 클린업 로직이 셋업 로직을 정확히 반영하는지 확인하는 테스트
      • 이로 인해 문제가 발생하면 클린업 함수를 구현
  2. 의존성 관리

    • 컴포넌트 내부에 정의된 객체 또는 함수가 의존성인 경우, Effect가 불필요하게 자주 실행될 수 있다
      • 불필요한 객체 및 함수 의존성을 제거하고, state 업데이트와 비반응형 로직을 Effect 외부로 추출

사용법

브라우저에서 화면을 다시 그리기 전 레이아웃 측정하기

  • 대부분의 컴포넌트는 위치와 크기를 몰라도 JSX만 반환하고 브라우저가 레이아웃을 계산한다.
  • 하지만 툴팁처럼 마우스오버 시 정확한 위치에 렌더링해야 하는 경우가 있다.
  • 툴팁의 올바른 위치를 결정하려면 툴팁의 높이를 알아야 한다.
  • 이를 위해 두 번의 패스로 렌더링 해야 한다.

1. 툴팁을 임의의 위치에 렌더링
- 초기 위치는 잘못될 수 있음

2. 툴팁의 높이를 측정하고 위치 결정
- 툴팁의 실제 높이를 측정

3. 툴팁을 올바른 위치에 다시 렌더링
- 측정된 높이를 바탕으로 툴팁을 정확한 위치에 재렌더링

  • 이 모든 작업은 브라우저가 화면을 다시 그리기 전에 이루어져야 한다.
    • 사용자가 툴팁이 움직이는 것을 보지 않기를 원하기 때문에
    • 브라우저가 화면을 다시 그리기 전에 useLayoutEffect를 호출하여 레이아웃 측정을 수행
function Tooltip() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0); 
  // 아직 실제 height 값을 모릅니다.

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height); 
    // Re-render now that you know the real height
	// 실제 높이를 알았으니 이제 리렌더링 합니다.
  }, []);

  // ...아래에 작성될 렌더링 로직에 tooltipHeight를 사용합니다...
}
  1. Tooltip은 초기 tooltipHeight = 0으로 렌더링됩니다
    • 따라서 툴팁의 위치가 잘못 지정될 수 있음
  2. React는 이를 DOM에 배치하고 useLayoutEffect에서 코드를 실행
  3. useLayoutEffect는 툴팁 콘텐츠의 높이를 측정하고 즉시 다시 렌더링을 촉발
  4. Tooltip이 실제 tooltipHeight로 다시 렌더링
    • 따라서 툴팁이 올바르게 배치
  5. React가 DOM에서 이를 업데이트하면 브라우저에 툴팁이 최종적으로 표시
  • Tooltip 컴포넌트가 두 번의 패스로 렌더링되어야 하지만, (먼저 tooltipHeight를 0으로 초기화한 다음 실제 측정된 높이) 최종 결과만 볼 수 있다는 점에 유의

useLayoutEffect는 브라우저가 다시 그리는 것을 차단

  • useLayoutEffect를 사용하면 브라우저가 화면을 다시 그리기 전에 코드와 상태 업데이트가 처리되도록 보장한다.

    • 이를 통해 툴팁을 렌더링하고, 측정한 후, 다시 렌더링할 수 있음
    • 이렇게 하면 첫 번째 렌더링을 사용자가 눈치채지 못하게 됨
    • useLayoutEffect는 브라우저의 페인팅을 차단

1. import 및 상태를 초기화

    import { useRef, useLayoutEffect, useState } from 'react';
    import { createPortal } from 'react-dom';
    import TooltipContainer from './TooltipContainer.js';

2. Tooltip 컴포넌트 생성

    export default function Tooltip({ children, targetRect }) {
      const ref = useRef(null);
      const [tooltipHeight, setTooltipHeight] = useState(0);

      useLayoutEffect(() => {
        const { height } = ref.current.getBoundingClientRect();
        setTooltipHeight(height);
      }, []);
  • ref를 사용해 툴팁의 DOM 요소를 참조한다
  • tooltipHeight 상태를 초기화한다
  • useLayoutEffect로 툴팁의 높이를 측정하여 tooltipHeight 상태를 업데이트한다
    3. 툴팁 위치 계산
      let tooltipX = 0;
      let tooltipY = 0;
      if (targetRect !== null) {
        tooltipX = targetRect.left;
        tooltipY = targetRect.top - tooltipHeight;
        if (tooltipY < 0) {
          tooltipY = targetRect.bottom;
        }
      }
  • targetRect가 존재하면 툴팁의 X, Y 좌표를 계산합니다.
  • 툴팁이 위쪽에 맞지 않으면 아래쪽에 배치합니다.

4. 툴팁 렌더링

      return createPortal(
        <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
          {children}
        </TooltipContainer>,
        document.body
      );
    }
  • createPortal을 사용하여 툴팁을 document.body에 렌더링합니다.
  • TooltipContainer에 위치와 내용(자식 요소)을 전달합니다.

5. 정리
1. Tooltip 컴포넌트는 useLayoutEffect를 사용해 툴팁의 높이를 측정
2. 이를 기반으로 툴팁의 위치를 계산하여 올바른 위치에 다시 렌더링
3. 이 과정에서 첫 번째 렌더링을 사용자가 눈치채지 못하게 브라우저의 페인팅을 차단

6.useLayoutEffect Vs useEffect

  • useLayoutEffect

    • 동기적 실행: useLayoutEffect는 DOM 업데이트 직후 즉시 실행
    • 이는 화면을 다시 그리기 전에 코드가 실행되어, 화면에 보이기 전에 필요한 작업들(예: 레이아웃 측정)을 처리할 수 있음을 의미
    • 따라서 사용자가 화면에서 변경 사항을 볼 때까지 대기하지 않고 바로 업데이트된 내용을 보여줄 수 있음
  • useEffect

    • 비동기적 실행: useEffect는 렌더링 후 비동기적으로 실행
    • 이는 DOM 업데이트 이후에 발생하므로, 화면에 바로 반영되지 않을 수 있음
    • 따라서 useEffect를 사용할 경우, 초기 렌더링 시에 UI가 제대로 조정되기 전에 사용자가 변경된 내용을 일시적으로 볼 수 있을 수 있음



























profile
웹 프론트엔드

0개의 댓글