react-slick custom arrow를 위한 타입 정의

tyange·2023년 9월 5일

Node.js 16 버전의 EOL이 다가옴에 따라(관련 문서 링크) 사내에서 서비스 중인 구버전 프로젝트 들도 업데이트를 해야 하는 상황이 왔다. 안타깝게도 CRA 등 현재는 잘 쓰이지 않는 툴로 빌드된 프로젝트가 대부분이라... 도무지 어떻게 해야 할지 감이 오질 않았다.

원래는 Vite 기반으로 빌드된 텅 빈 프로젝트에 구버전 프로젝트의 파일들을 복사, 붙여넣기 하는 식으로 작업을 할 예정이었다... 하지만 이건 단순히 개발 툴만 변경하는 작업이 아니라, Dependency의 호환성 등 따져봐야할 게 만만치 않은 작업이었다.

고민 끝에 결국은 Next.js로 migration 하기로 하고 작업을 진행하고 있었다. 그런데...

웬놈의 carousel이 이렇게 많이 필요한지... 메인 페이지에만 carousel이 2개나 있었다. 구버전 프로젝트에서 모두 react-slick으로 구현되었기에, 나도 똑같이 구현하기로 했다.

그 중에 custom-arrow, 즉 다음, 이전 버튼을 커스텀해서 구현한 carousel이 있었는데, 아무리 버튼을 눌러도 다음, 이전으로 이동이 되질 않았다. 당시 나는 아래처럼 SliderButton을 구현해서 쓰고 있었다.

type SliderButtonProps = {
  children: ReactNode;
  isRight: boolean;
}

export default function SliderButton({
  children,
  isRight,
}: SliderButtonProps) {
  return (
    <div
      className={`w-12 h-12 absolute top-1/2 -translate-y-1/2 block z-30 ${
        isRight ? "right-3" : "left-3"
      }`}
    >
      <button
        className="w-full h-12 relative rounded-full bg-white shadow-sm border flex justify-center items-center"
      >
        {children}
      </button>
    </div>
  );
}

곰곰 생각해보니, 당연하게도 onClick 이벤트 핸들러를 부착하지 않은 버튼이니 응답이 없는 게 당연하지 않을까, 싶었다. 다음, 이전으로 슬라이더를 이동시키는 메서드는 당연히 react-slick 안에 있겠지 싶어서 열심히 뒤져보았다.

declare class Slider extends React.Component<Settings, never> {
    innerSlider?: InnerSlider | undefined;
    slickNext(): void;
    slickPause(): void;
    slickPlay(): void;
    slickPrev(): void;
    slickGoTo(slideNumber: number, dontAnimate?: boolean): void;
}

실제로 @types/react-slick의 안내를 받아 react-slick 슬라이더의 import 구문으로 깊이 들어가보면, 위와 같은 타입 정의를 만날 수 있다. 하지만 아무리 import 구문을 이리저리 바꾸며 용을 써도, slickNext()slickPrev() 같은 메서드에 접근을 할 수가 없었다.
그렇다면 공식 문서엔 custom-arrow에 관하여 어떻게 설명하고 있을까?

function SampleNextArrow(props) {
  const { className, style, onClick } = props;
  return (
    <div
      className={className}
      style={{ ...style, display: "block", background: "red" }}
      onClick={onClick}
    />
  );
}

function SamplePrevArrow(props) {
  const { className, style, onClick } = props;
  return (
    <div
      className={className}
      style={{ ...style, display: "block", background: "green" }}
      onClick={onClick}
    />
  );
}

export default class CustomArrows extends Component {
  render() {
    const settings = {
      dots: true,
      infinite: true,
      slidesToShow: 3,
      slidesToScroll: 1,
      nextArrow: <SampleNextArrow />,
      prevArrow: <SamplePrevArrow />
    };
    return (
      <div>
        <h2>Custom Arrows</h2>
        <Slider {...settings}>
          <div>
            <h3>1</h3>
          </div>
          <div>
            <h3>2</h3>
          </div>
          <div>
            <h3>3</h3>
          </div>
          <div>
            <h3>4</h3>
          </div>
          <div>
            <h3>5</h3>
          </div>
          <div>
            <h3>6</h3>
          </div>
        </Slider>
      </div>
    );
  }
}

위 코드는 공식 문서에 나온 코드를 거의 전문 그대로 옮긴 것이다. 신기한 점은 settings 으로 선언된 변수를 Slider에 주입하는 코드다. 해당 코드에 선언된 SampleNextArrow, SamplePrevfArrow 컴포넌트에 별다른 props를 전달하지 않는다.
그렇다면 propsonClick과 같은 이벤트 핸들러를 따로 전달하지 않아도 자동적으로 전달(?)이 되는 것인가 하는 합리적인 의심이 들었다.
그래서 SliderButton 컴포넌트의 props 타입 정의를 아래와 같이 바꾸었다.

interface SliderButtonProps extends CustomArrowProps {
  children: ReactNode;
  isRight: boolean;
}

extends 구문을 써서 react-slick에서 import한 CustomArrowProps 타입으로 확장시켜 주었다. 이제 자동으로 props로 전달받는 onClick 이벤트 핸들러를 반환되는 JSX 구문에 연결시켜 주기만 하면 된다.

export default function SliderButton({
  children,
  isRight,
  onClick,
}: SliderButtonProps) {
  return (
    <div
      className={`w-12 h-12 absolute top-1/2 -translate-y-1/2 block z-30 ${
        isRight ? "right-3" : "left-3"
      }`}
    >
      <button
        onClick={onClick}
        className="w-full h-12 relative rounded-full bg-white shadow-sm border flex justify-center items-center"
      >
        {children}
      </button>
    </div>
  );
}

최종적으로 Slider를 아래처럼 사용하게 된다. 이때, 다음, 이전과 관련된 메서드는 생각할 필요가 없다. 자동(?)으로 전달되기 때문이다.

		<Slider
          infinite
          autoplay
          arrows
          slidesToShow={6}
          nextArrow={
            <SliderButton isRight={false}>
              <Image
                alt="리뷰 리스트 버튼"
				src="https://test.com"
                width="13"
                height="13"
              />
            </SliderButton>
          }
          prevArrow={
            <SliderButton isRight={true}>
              <Image
                alt="리뷰 리스트 버튼"
				src="https://test.com"
                width="13"
                height="13"
              />
            </SliderButton>
          }
        >
          {reviewData.data.map((review) => (
            <ReviewListItem key={review.title} review={review} />
          ))}
        </Slider>
profile
아주 흐린 날의 기록

0개의 댓글