ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Typescript] Generic과 forwardRef
    프로그래밍 언어/TypeScript 2024. 4. 16. 11:29
    개요

    1. 제네릭함수와 forwardRef의 문제점
    2. 해결 방안

     

    ✅ 제네릭함수와 forwardRef의 문제점


    📦 기본적인 예제 (Table)

    const Table = <T,>(props: {
      data: T[];
      renderRow: (row: T) => React.ReactNode;
    }) => {
      return (
        <table>
          <tbody>
            {props.data.map((item, index) => (
              <props.renderRow key={index} {...item} />
            ))}
          </tbody>
        </table>
      );
    };
    • 위 예제는 간단히 제네릭함수를 통해서 구현된 Table이다. 현재 Table 컴포넌트를 사용하는 것을 통해서 어떻게 추론이 되는지 알아보겠습니다.
    <Table
      // 1. Data is a string here...
      data={["a", "b"]}
      // 2. So ends up inferring as a string in renderRow.
      renderRow={(row) => {
                  
    (parameter) row: string
        return <tr>{row}</tr>;
      }}
    />;
    • 여기서 알 수 있듯이 data로 전달된 Array형태에서 타입이 추론이 되고, 그 추론된 타입이 renderRow에 전달되어 다양한 상황에서 사용이 가능해집니다.

    🎉 <T>, <T,> 뭔 차이인데!

    더보기

    이는 Typscript와 JSX 문법 간 충돌이 발생하여 일어나는 상황이다. 구문적으로 해당 위치에 <T>를 위치시킬 경우 이를 변수에 JSX문법을 할당한 구조처럼 보이기에 이러한 문법 간 충돌이 발생하여 제대로 인식하지 못하는 상황이다 그렇기에 우리는 제네릭함수로 사용하고 있다고 알리기 위해 <T,>와 같이 ","를 붙여 표시를 해준다고 생각하면 된다.

     

    📦 forwardRef의 제한사항

    forwardRef는 Typescript와 함께 쓰일 때 제네릭 컴포넌트에서 사용된 interface를 추론할 수 없다는 큰 문제점이 있다.
    이로 인해서 forwardRef와 제네릭함수를 같이 사용하기 위해서는 별도의 조치가 필요하다.

     

    ref 적용 예제)

    const Table = <T,>(
      props: {
        data: T[];
        renderRow: (row: T) => React.ReactNode;
      },
      ref: React.ForwardedRef<HTMLTableElement>
    ) => {
      return (
        <table ref={ref}>
          <tbody>
            {props.data.map((item, index) => (
              <props.renderRow key={index} {...item} />
            ))}
          </tbody>
        </table>
      );
    };
     
    const ForwardReffedTable = React.forwardRef(Table);
    • 단순히 이와 같이 적용하게 된다면 추론을 못하는 불상사가 발생한다..
    • 앞서 말했듯이 forwardRef로 인해 추론이 제대로 되지 않는 문제 때문이다.

     

     



     

    📦 해결 방안 

    다른 타입정의를 이용해서 forwardRef를 재정의하는 방법

     

    function fixedForwardRef<T, P = {}>(
      render: (props: P, ref: React.Ref<T>) => React.ReactNode
    ): (props: P & React.RefAttributes<T>) => React.ReactNode {
      return React.forwardRef(render) as any;
    }
    
    export const ForwardReffedTable = fixedForwardRef(Table);
    • props의 경우에는 구조분해 할당으로 전달되는 값이기에 기본적으로 object의 구조를 띄고 있다 그렇기에 전달 되는 P는 {}로 설정을 해놓는다
    • ref는 할당되는 tag의 종류에 따라서 type이 변동되기에 이를 T라고 정의하고 전달을 해준다.
    • 이렇게 전달된 render의 경우 JSX문법을 반환하므로 React.ReactNode를 반환하게끔 render의 타입을 정의한다.
    • (props: P & React.RefAttributes<T>) => React.ReactNode 이부분은 최종적으로 return하게 되는 타입으로 전달되는 컴포넌트의 인자로 P에 대한 props와 ref를 받을 수 있다.

     

    ⭐️ 실제 구현 사항

    실제 캐러셀을 구현하면서 타입을 정의하는 부분이 막혔지만 위와 같은 방법을 통해서 구현이 가능했다

    더보기
    import { Swiper, SwiperSlide } from 'swiper/react'
    import { ComponentType, ForwardedRef, RefAttributes, forwardRef } from 'react'
    import { SwiperOptions } from 'swiper/types'
    
    import 'swiper/css'
    import 'swiper/css/grid'
    import 'swiper/css/pagination'
    import 'swiper/css/navigation'
    import usePagination from '@/hooks/usePagination'
    
    interface CarouselProps<T> {
      Component: ComponentType<T>
      itemList: T[]
      options?: SwiperOptions
      minWidth?: number
      swiperStyle?: object
    }
    
    const InnerCarousel = <T extends object>(
      { Component, itemList, options, swiperStyle }: CarouselProps<T>,
      ref: ForwardedRef<HTMLDivElement>,
    ) => {
      const { handleNextPage, handlePrevPage } = usePagination()
      return (
        <div
          ref={ref}
          style={{
            position: 'relative',
            alignContent: 'center',
            justifyContent: 'center',
            padding: '10px 50px',
            display: 'felx',
          }}
        >
          <Swiper {...options} style={swiperStyle}>
            {itemList.map((item, index) => (
              <SwiperSlide key={index}>
                <Component {...item} />
              </SwiperSlide>
            ))}
          </Swiper>
          <button className="swiper-button-next" onClick={handleNextPage} />
          <button className="swiper-button-prev" onClick={handlePrevPage} />
        </div>
      )
    }
    
    function fixedForwardRef<T, P = object>(
      render: (props: P, ref: React.Ref<T>) => React.ReactNode,
    ): (props: P & React.RefAttributes<T>) => React.ReactNode {
      return forwardRef(render) as (props: P & RefAttributes<T>) => React.ReactNode
    }
    
    const Carousel = fixedForwardRef(InnerCarousel)
    export default Carousel

     

     

     

    댓글

Designed by Tistory.