ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Next.js 14][Swiper] 스와이프 가능한 카드 리스트 만들기 (Carousel Slider)
    웹 개발/Nextjs 14 2024. 3. 13. 02:42

    포스팅을 너무 각 잡고 쓰려니까 오히려 안 쓰게 돼서, 아예 간단하게 작성하는 버그 해결 / 궁금한 점 해소 용 카테고리를 하나 팠다.

    암튼 이 편한 카테고리의 첫 포스팅은 Swiper, 너로 정했다!

    쓰고 보니 본 카테고리로 올려도 될 것 같아 격상시켰다. 렛츠고.

     

    구현해야 하는 UI

    사용자가 등록한 카드를 아래의 형태로 보여주고, 슬라이드 / 화살표 버튼을 이용하여 변경하는 UI를 작성하고자 한다.

    이런 형태를 보통 Carousel Slider라고 하는데,

    위의 키워드랑 Card list 등등의 키워드로 찾아본 라이브러리 중에서는 확장하기 편함 + 쓰기 쉬운 놈을 못 찾았다.

     

    React Swiper

    처음부터 내 입맛대로 만드는 게 마음 편하긴 하지만, 내가 개발할 수 있는 시간은 한정되어 있어서 아래의 라이브러리를 이용했다.

     

    Swiper - The Most Modern Mobile Touch Slider

    Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.

    swiperjs.com

    Carousel 관련 속성이 대놓고 있는 건 아니지만, 제공되고 있는 여러 속성을 적절하게 조합하면 충분히 만들 수 있을 것 같았고,

    시도해본 결과 성공했다!

    일단 결과물부터 공유하면 이렇다.

     

     

    nextjs 14(app router)에서는 어떻게 적용시킬 수 있는지에 대한 레퍼런스는 쉽게 찾을 수 없었지만,

    꽤나 탄탄하게 설명된 라이브러리라 충분히 documentation을 보고 코드를 작성할 수 있었다.

    처음엔 살짝 감이 안 잡혔는데, gitHub 코드 보고 바로 이해했다.

    다른 분들을 위해 내가 깨달음을 얻은 코드를 공유해보겠다. (사실 떠먹여준거나 마찬가지)

     

    아래는 해당 Swiper 라이브러리를 사용해서 내가 작성한 코드이다.

    보면 onSlideChange 속성에 함수를 추가하여, swiper의 현재 선택된 인덱스에 해당하는 슬라이드의 스타일을 다르게 하였다.

    감히 가장 최신 버전 코드라고 할 수 있음(물론 2024.03.13. 기준)

     

    코드

    // CardCarouselSlider.tsx
    
    import React, {useEffect, useState} from 'react'
    import { Swiper, SwiperSlide } from 'swiper/react';
    import { Navigation, Pagination } from "swiper/modules";
    
    import CardComponent from '../button/CardButton'
    
    import "swiper/css";
    import "swiper/css/pagination";
    import "swiper/css/navigation";
    import {getCardInfo} from "@/utils/api/getBillingInfo";
    
    export interface CardInfoProp {
        name: string;
        cardNumber: string;
        cardExpirationYear: string;
        cardExpirationMonth: string;
        cardPassword: string;
    }
    
    const CardList = ({onAddition}: {onAddition:Function}) => {
        const [refresh, setRefresh] = useState<boolean>(true);
        const [data, setData] = useState<CardInfoProp[]>();
        const [currentCard, setCurrentCard] = useState<number>(0);
    
        useEffect(() => {
            if (refresh)
            {
                getCardInfo(setData(cardList));
                setRefresh(false);
            }
        }, [refresh])
    
        const addCard = () => {
            onAddition();
        }
    
        return (
            <>
                <Swiper
                    spaceBetween={10}
                    slidesPerView={1.5}
                    simulateTouch={true}
                    grabCursor={true}
                    centeredSlides={true}
                    initialSlide={currentCard}
                    onSlideChange={(swiper) => {
                        setCurrentCard(swiper.snapIndex);
                    }}
                    observer={true}
                    navigation={true}
                    pagination={true}
                    modules={[Navigation, Pagination]}
                >
                    {data?.map((item, index) => {
                        return (
                            <SwiperSlide key={index} className={"items-center"}>
                                <CardComponent card={item} onSelect={() => alert(item.cardNumber)} focused={currentCard === index}/>
                            </SwiperSlide>
                        )})}
                    <SwiperSlide key={data ? data.length : 0} className={"items-center cursor-pointer"}>
                        <button className={`card`} onClick={addCard}>
                            추가하기
                        </button>
                    </SwiperSlide>
    
                </Swiper>
            </>
    
        )
    }
    
    export default CardList;
    // CardComponent.tsx
    
    import "@/styles/components/card.css";
    
    const CardComponent = ({card, onSelect, focused}:CreditCardProp) => {
        return (
            <>
                <button className={`card ${focused && "focused"}`} onClick={focused ? () => onSelect() : undefined}>
                    <div className="card-info">
                        <p className="card-name">{card.name}</p>
                        <div className="card-info--details">
                            <p className="card-exp-date">{card.cardExpirationMonth}/{card.cardExpirationYear} 만료</p>
                            <p className="card-number">****-****-****-{card.cardNumber.slice(-4)}</p>
                        </div>
                    </div>
                    {
                        focused &&
                        <button className="additional-button" onClick={() => console.log("delete")}>***</button>
                    }
                </button>
            </>
        )
    }
    
    export default CardComponent;
    /* card.css */
    
    @tailwind base;
    
    .card {
      @apply transition-colors duration-500 w-full h-40 bg-gray-100 border border-gray-200 rounded-xl cursor-default p-5 text-left;
    }
    
    .card-info {
      @apply flex flex-col h-full;
    }
    
    .card-info p {
      @apply transition-colors duration-500 text-gray-200;
    }
    .card-info .card-name {
      @apply font-medium text-xl;
    }
    .card-info .card-exp-date {
      @apply font-medium text-sm;
    }
    .card-info .card-number {
      @apply font-medium text-base;
    }
    
    .card-info--details {
      @apply flex flex-col gap-y-2;
    }
    
    .card.focused {
      @apply bg-white cursor-pointer;
    }
    .card.focused .card-info p {
      @apply text-gray-800;
    }
    
    .card .additional-button {
      @apply absolute top-0 right-0 m-5;
    }

     

    끗~