CS/프로그래밍

[Typescript] Generic과 forwardRef

고래강이 2024. 4. 17. 20:10
개요

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

 

 

 

'CS > 프로그래밍' 카테고리의 다른 글

Jotai  (0) 2024.05.08
JS, StructuredClone()  (1) 2024.05.01
DX  (2) 2024.04.03
useLayoutEffect의 활용  (2) 2024.04.02
Zustand란 무엇인가?  (0) 2024.03.23