StudyForYou / ouahhan-typescript-with-react

우아한 타입스크립트 with 리액트 스터디 레포 🧵
4 stars 0 forks source link

#28 [9장_2] 유용하게 사용 중인, 혹은 새롭게 알게 된 커스텀 훅을 알려주세요. 해당 훅에 만약 타입에 대한 설정이 있다면 같이 알려주세요! #43

Closed hyeyoonS closed 2 months ago

hyeyoonS commented 2 months ago

❓문제

1) 유용하게 사용 중인, 혹은 새롭게 알게 된 커스텀 훅을 알려주세요. 2) 해당 훅에 만약 타입에 대한 설정이 있다면 같이 알려주세요!

🎯답변

usehooks-ts 라이브러리 문서에 있는 useResizeObserver 커스텀 훅을 가져왔습니다.

[목적] 요소의 크기 변경을 감지하여, onResize 콜백을 실행시키고 현재 크기를 반환합니다.

[작동 원리]

  1. ResizeObserver의 콜백은 observe 하는 요소의 크기가 변경될 때마다 콜백 함수를 호출한다. 이 때 콜백 함수에서 새로운 너비, 높이를 계산하여 이전 크기와 비교해 변경이 발생했는지 확인합니다.
  2. 크기 변경이 감지되면 onResize 콜백이 호출되고 width, height 상태가 업데이트 됩니다.
  3. width, height 상태를 반환하여 현재 요소의 크기를 알 수 있습니다.
  4. cleanup 함수에서 observe를 해제하여 더 이상 크기 변경을 감지하지 않도록 합니다.

[타입 원리]

[코드]

import { useEffect, useRef, useState } from 'react'

import type { RefObject } from 'react'

import { useIsMounted } from 'usehooks-ts'

type Size = {
  width: number | undefined
  height: number | undefined
}

type UseResizeObserverOptions<T extends HTMLElement = HTMLElement> = {
  ref: RefObject<T>
  onResize?: (size: Size) => void
  box?: 'border-box' | 'content-box' | 'device-pixel-content-box'
}

const initialSize: Size = {
  width: undefined,
  height: undefined,
}

export function useResizeObserver<T extends HTMLElement = HTMLElement>(
  options: UseResizeObserverOptions<T>,
): Size {
  const { ref, box = 'content-box' } = options
  const [{ width, height }, setSize] = useState<Size>(initialSize)
  const isMounted = useIsMounted()
  const previousSize = useRef<Size>({ ...initialSize })
  const onResize = useRef<((size: Size) => void) | undefined>(undefined)
  onResize.current = options.onResize

  useEffect(() => {
    if (!ref.current) return

    if (typeof window === 'undefined' || !('ResizeObserver' in window)) return

    const observer = new ResizeObserver(([entry]) => {
      const boxProp =
        box === 'border-box'
          ? 'borderBoxSize'
          : box === 'device-pixel-content-box'
            ? 'devicePixelContentBoxSize'
            : 'contentBoxSize'

      const newWidth = extractSize(entry, boxProp, 'inlineSize')
      const newHeight = extractSize(entry, boxProp, 'blockSize')

      const hasChanged =
        previousSize.current.width !== newWidth ||
        previousSize.current.height !== newHeight

      if (hasChanged) {
        const newSize: Size = { width: newWidth, height: newHeight }
        previousSize.current.width = newWidth
        previousSize.current.height = newHeight

        if (onResize.current) {
          onResize.current(newSize)
        } else {
          if (isMounted()) {
            setSize(newSize)
          }
        }
      }
    })

    observer.observe(ref.current, { box })

    return () => {
      observer.disconnect()
    }
  }, [box, ref, isMounted])

  return { width, height }
}

type BoxSizesKey = keyof Pick<
  ResizeObserverEntry,
  'borderBoxSize' | 'contentBoxSize' | 'devicePixelContentBoxSize'
>

function extractSize(
  entry: ResizeObserverEntry,
  box: BoxSizesKey,
  sizeType: keyof ResizeObserverSize,
): number | undefined {
  if (!entry[box]) {
    if (box === 'contentBoxSize') {
      return entry.contentRect[sizeType === 'inlineSize' ? 'width' : 'height']
    }
    return undefined
  }

  return Array.isArray(entry[box])
    ? entry[box][0][sizeType]
    : // @ts-ignore Support Firefox's non-standard behavior
    (entry[box][sizeType] as number)
}
drizzle96 commented 2 months ago

usehooks-ts 라이브러리 문서에 있는 useResizeObserver 커스텀 훅을 가져왔습니다.

[목적] 요소의 크기 변경을 감지하여, onResize 콜백을 실행시키고 현재 크기를 반환합니다.

[작동 원리]

  1. ResizeObserver의 콜백은 observe 하는 요소의 크기가 변경될 때마다 콜백 함수를 호출한다. 이 때 콜백 함수에서 새로운 너비, 높이를 계산하여 이전 크기와 비교해 변경이 발생했는지 확인합니다.
  2. 크기 변경이 감지되면 onResize 콜백이 호출되고 width, height 상태가 업데이트 됩니다.
  3. width, height 상태를 반환하여 현재 요소의 크기를 알 수 있습니다.
  4. cleanup 함수에서 observe를 해제하여 더 이상 크기 변경을 감지하지 않도록 합니다.

[타입 원리]

[코드]

import { useEffect, useRef, useState } from 'react'

import type { RefObject } from 'react'

import { useIsMounted } from 'usehooks-ts'

type Size = {
  width: number | undefined
  height: number | undefined
}

type UseResizeObserverOptions<T extends HTMLElement = HTMLElement> = {
  ref: RefObject<T>
  onResize?: (size: Size) => void
  box?: 'border-box' | 'content-box' | 'device-pixel-content-box'
}

const initialSize: Size = {
  width: undefined,
  height: undefined,
}

export function useResizeObserver<T extends HTMLElement = HTMLElement>(
  options: UseResizeObserverOptions<T>,
): Size {
  const { ref, box = 'content-box' } = options
  const [{ width, height }, setSize] = useState<Size>(initialSize)
  const isMounted = useIsMounted()
  const previousSize = useRef<Size>({ ...initialSize })
  const onResize = useRef<((size: Size) => void) | undefined>(undefined)
  onResize.current = options.onResize

  useEffect(() => {
    if (!ref.current) return

    if (typeof window === 'undefined' || !('ResizeObserver' in window)) return

    const observer = new ResizeObserver(([entry]) => {
      const boxProp =
        box === 'border-box'
          ? 'borderBoxSize'
          : box === 'device-pixel-content-box'
            ? 'devicePixelContentBoxSize'
            : 'contentBoxSize'

      const newWidth = extractSize(entry, boxProp, 'inlineSize')
      const newHeight = extractSize(entry, boxProp, 'blockSize')

      const hasChanged =
        previousSize.current.width !== newWidth ||
        previousSize.current.height !== newHeight

      if (hasChanged) {
        const newSize: Size = { width: newWidth, height: newHeight }
        previousSize.current.width = newWidth
        previousSize.current.height = newHeight

        if (onResize.current) {
          onResize.current(newSize)
        } else {
          if (isMounted()) {
            setSize(newSize)
          }
        }
      }
    })

    observer.observe(ref.current, { box })

    return () => {
      observer.disconnect()
    }
  }, [box, ref, isMounted])

  return { width, height }
}

type BoxSizesKey = keyof Pick<
  ResizeObserverEntry,
  'borderBoxSize' | 'contentBoxSize' | 'devicePixelContentBoxSize'
>

function extractSize(
  entry: ResizeObserverEntry,
  box: BoxSizesKey,
  sizeType: keyof ResizeObserverSize,
): number | undefined {
  if (!entry[box]) {
    if (box === 'contentBoxSize') {
      return entry.contentRect[sizeType === 'inlineSize' ? 'width' : 'height']
    }
    return undefined
  }

  return Array.isArray(entry[box])
    ? entry[box][0][sizeType]
    : // @ts-ignore Support Firefox's non-standard behavior
    (entry[box][sizeType] as number)
}