4. 타입스크립트 forwardRef의 활용

Sal Jeong·2022년 7월 28일
0

열심히 코딩을 하는 중, IntersectionObserver Api를 활용한 Scroll-Spying 및 애니메이션을 바닐라고 한번 다시 구현해볼 기회가 있었다.

이전 Bootstrap을 사용할 때는 ScrollSpy라는
https://getbootstrap.com/docs/4.0/components/scrollspy/
기능을 사용해서 구현했었기에, 한번 바닐라로 만들어보면 좋을 거라는 생각이 들었다.

만들고자 하는 것은 다음과 같다.

IntersectionObserver Api을 활용해서, 현재 활성화된(브라우저에서 보이는)component를 감지해서, 투명도를 늘려주는 Trasition 애니메이션을 실행시키는 것.

https://usehooks-ts.com/react-hook/use-intersection-observer

에서 해당 Api를 사용하는 example은 쉽게 찾을 수 있었다.


// useIntersectionObserver.tsx

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

interface Args extends IntersectionObserverInit {
  freezeOnceVisible?: boolean
}

function useIntersectionObserver(
  
  elementRef: RefObject<Element>,
  {
    threshold = 0,
    root = null,
    rootMargin = '0%',
    freezeOnceVisible = false,
  }: Args,
): IntersectionObserverEntry | undefined {
  const [entry, setEntry] = useState<IntersectionObserverEntry>()

  const frozen = entry?.isIntersecting && freezeOnceVisible

  const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
    setEntry(entry)
  }

  useEffect(() => {
    const node = elementRef?.current // DOM Ref
    const hasIOSupport = !!window.IntersectionObserver

    if (!hasIOSupport || frozen || !node) return

    const observerParams = { threshold, root, rootMargin }
    const observer = new IntersectionObserver(updateEntry, observerParams)

    observer.observe(node)

    return () => observer.disconnect()

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [elementRef, JSON.stringify(threshold), root, rootMargin, frozen])

  return entry
}

export default useIntersectionObserver

//SectionRenderer.tsx

import { useIntersectionObserver } from 'usehooks-ts'

const Section = (props: { title: string }) => {
  const ref = useRef<HTMLDivElement | null>(null)
  const entry = useIntersectionObserver(ref, {})
  const isVisible = !!entry?.isIntersecting

  console.log(`Render Section ${props.title}`, { isVisible })

  return (
    <div
      ref={ref}
    >
    ,,,
      
    </div>
  )
}

위 hooks를 그대로 SectionRenderer.tsx에 사용하여도 크게 문제는 없겠지만, 내가 하고싶은 것은 SectionRenderer 상위에서 Ref를 handling하는 것이다. 맨 상위 index에서 스크롤링 기능까지 구현할 생각이기 때문에...

우선 위 SectionRenderer를 React.fowardRef로 만드는것이 가장 먼저가 될 것이다.

https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forward_and_create_ref
의 내용을 참고하여,

// forwarded Renderer

import useIntersectionObserver from './hooks/UseIntersectionObserver'

// const workRef = useRef<HTMLDivElement | null>(null);
// 위 index에서 내려오는 Ref를 React.forwardRef hoc로 감싸고, 타입은 ref와 같은 element로 잡는다.

type RefType = HTMLDivElement | null;

const SectionRenderer = forwardRef<RefType, SectionProps>(({ type = 'work' }, ref) => {

    //이 부분 넘어갈때는 다르게 넘겨줘야 함!
    const entry = useIntersectionObserver(ref, {})
    const isVisible = !!entry?.isIntersecting
    return (
      ,,,
    )
}

문제는, 위 Return Type 패턴으로 짜여져 있는 useIntersectionObserver에 어떻게 이 ref를 넘기냐 하는 것이다.
예전에 개발할 때 이런식으로 Custom hook에 ref를 넘기다가, type 오류를 해결 못하고 그냥 any 타입으로 지정해놓은적이 한번 있었기 때문에...

그때처럼 그대로 ForwardedRef를 RefObject로 받을 수 없다는 에러가 나타난다.

따라서

  1. UseIntersectionObserver에서는 위 forwardedRef를 RefObject로 받기 때문에 이를 그대로 forwardedRef로 받게 한다.
  2. 경험상 forwardedRef에서 .current를 조회할 때 역시 에러가 떳으므로 이 부분도 해결해 주면 되겠다.
// new UseIntersectionObserver

// 기존 HTMLDivElement에서, ForwardedRef 타입으로 변경
interface RefProps {
ref: React.ForwardedRef<HTMLDivElement>;
}

function useIntersectionObserver(
elementRef: RefProps,
{ threshold = 0, root = null, rootMargin = "0%", freezeOnceVisible = false }: Args
): IntersectionObserverEntry | undefined {
const [entry, setEntry] = useState<IntersectionObserverEntry>();



const frozen = entry?.isIntersecting && freezeOnceVisible;

const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
  setEntry(entry);
};

useEffect(() => {

  //forwardedRef에서 current를 조회하기 위해, 타입 명시를 RefObject로 assert해 준다.
  const asserted = elementRef as RefObject<HTMLDivElement>;
  const node = asserted.current; // DOM Ref
  const hasIOSupport = !!window.IntersectionObserver;
,,,

와 같은 방법으로 해결할 수 있었다.
타입스크립트를 해나가면서 느끼는 점은, 아직 '왜'에 대한 해답이 좀 부족하다는 것을 느낀다. 위 Cheetsheet나 여러 Stackoverflow 답변들을 보면서 해결하고는 있지만 조금 더 React types를 잘 설명하는 그런 위키가 있으면 좋지 않을까..

profile
Can an old dog learn new tricks?

0개의 댓글