프로젝트 이모저모/HoduMarket 프로젝트

오픈마켓) 자동,페이지네이션 캐러셀 슬라이드 만들기 (feat. 리액트/타입스크립트)

Ella Seon 2023. 4. 17. 11:28

0. 자동 캐러셀 슬라이드 만들기(feat: 리액트 스타일드 컴포넌트/타입스크립트)

아래 움짤처럼 자동으로 옆페이지로 넘어가며, 

페이지네이션 버튼도 적용하려고 한다.

 

즉, 필요한 것

1) 페이지 네이션 : 버튼을 누르면 옆 슬라이드로 넘어가기

2) 일정 시간이 지나면 자동으로 옆 슬라이드로 넘어가기

1. 코드

1) 먼저 이미지 4장부터 준비하고, public 폴더에 넣는다.

✅ public 폴더에 이미지를 넣는 경우는? 

1) public 폴더는 이미지를 정적 자원으로 처리하기 위함이다. 
리액트로 개발을 끝내면 build라는 작업(지금까지 짰던 코드를 한파일로 압축) 을 한다.
src 폴더에 있던 코드와 파일은 다 압축이 되는데, public 폴더에 있는 것들은 그대로 보존해준다. 
그래서 형태를 보존하고 싶은 파일은 public 폴더에 넣으면 되는데, js파일은 동적으로 변하니 넣을일이 없고, 이미지,txt 등 수정이 필요없는 static 파일들 경우엔 public 폴더에 보관한다.

2) public 폴더에 보관하면 애플리케이션의 성능을 향상시킬 수 있다.
- 이미지 파일을 public 폴더에 넣어 빌드 시점에서 한 번 복사되면, 클라이언트 측에서 이미지 파일을 불러올 때 서버에 요청하지 않고 브라우저 캐시에 저장된 이미지 파일을 사용할 수 있다. 이렇게 되면, 이미지 파일을 불러오는 네트워크 요청 수를 줄일 수 있어서 웹 애플리케이션의 로딩 속도를 향상시킬 수 있다.
- React 애플리케이션에서는 Webpack과 같은 번들러를 사용하여 애플리케이션의 자원들을 번들 파일(bundle file)로 묶어서 제공한다. 번들 파일은 JavaScript, CSS, 이미지 등 여러 종류의 파일들을 포함할 수 있다. 이때 이미지 파일을 번들 파일에 포함시키면, 번들 파일의 용량이 커지는 단점이 있다. 이미지 파일을 public 폴더에 넣으면 번들 파일에 포함시키지 않아 번들 파일의 용량을 줄일 수 있다. 따라서, 번들 파일의 용량을 줄일 수 있어서 애플리케이션의 로딩 속도가 개선된다. 

2) 목업데이터를 만든다.

- 추후 map 으로 돌려서 나열시켜야 해서 임시 데이터를 만들었다.

//캐러셀 임시 데이터 (원래는 서버에서 받아와야 한다!)

export const carouselData = [
  { id: 1, title: "image 1", alt: "달걀후라이 반찬" },
  { id: 2, title: "image 2", alt: "PUMA 운동화" },
  { id: 3, title: "image 3", alt: "널부러져있는 레몬" },
  { id: 4, title: "image 4", alt: "화장품" },
];

3) 캐러셀 구조를 만들어보자

- 보통은 css 를 짤 때, 이미지 4개를 display:flex 로 처리해서 위치를 이동시키는 방식으로 구현한다.

- 하지만, 이번에는 이미지 4장을 모두 한곳에 겹쳐서 위치시키고 이미지가 활성화 될때 투명도는 1, 비활성화일때 투명도 0으로 설정해놓고 슬라이드 전환 시 이미지가 서서히 나타나는 효과를 구현했다. 

// style.ts

// 캐러셀의 전체 레이아웃
export const CarouselLayout = styled.div`
  position: relative;
  height: 500px;
  background-color: #f2f2f2;
  overflow: hidden; // 캐러셀 영역 밖의 이미지가 보이지 않게 함
`;

// 각 이미지를 감싸는 div 요소의 스타일
export const ImgBox = styled.div<{ opacity: string }>`
  position: absolute;
  width: 100%;
  height: 100%;
  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: ${(props) => (props.opacity === "active" ? "1" : "0")};
    transition: opacity ease-in-out 0.4s;
  }
`;
// Carousel.tsx
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import * as S from "./style";
import { carouselData } from "./carouselData";

function Carousel() {
  const [carouselIndex, setCarouselIndex] = useState(1);
  
  return (
    <>
      <S.CarouselLayout>
        {carouselData.map((item, index) => {
          return (
            <Link to="#" key={item.id}>
              <S.ImgBox
                opacity={carouselIndex === index + 1 ? "active" : "none"}
              >
                <img
                  src={process.env.PUBLIC_URL + `/Imgs/image${index + 1}.jpg`}
                  alt={item.alt}
                />
              </S.ImgBox>
            </Link>
          );
        })}
      </S.CarouselLayout>
    </>
  );
}

export default Carousel;

- 위 Carousel.tsx 파일을 보자

public 폴더에서 이미지를 가져올때는 굳이 import 할 필요 없이 그냥 아래와 같은 방식으로 하면 된다.

process.env.PUBLIC_URL /이미지경로

4) 페이지네이션 할 화살표 이미지를 불러오자

// style.ts

export const ArrowBtn = styled.button<{ direct: string }>`
  position: absolute;
  left: ${(props) => props.direct === "left" && "0px"};
  right: ${(props) => props.direct === "right" && "0px"};
  top: 50%;
  transform: translateY(-50%);
  width: 100px;
  height: 100%;
  background: transparent;
  cursor: pointer;
`;
// Carousel.tsx

import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import * as S from "./style";
import { carouselData } from "./carouselData";
import { ReactComponent as LeftArrow } from "../../../assets/images/icon-swiper-left.svg";
import { ReactComponent as RightArrow } from "../../../assets/images/icon-swiper-right.svg";

function Carousel() {
  const [carouselIndex, setCarouselIndex] = useState(1);

  useEffect(() => {
    const timer = setInterval(() => {
      moveNextImg();
    }, 5000);

    return () => {
      clearInterval(timer);
    };
  }, [carouselIndex]);

  // 화살표 버튼 이후 이미지
  const moveNextImg = () => {
    if (carouselIndex !== carouselData.length) {
      setCarouselIndex(carouselIndex + 1);
    } else {
      setCarouselIndex(1);
    }
  };
  
  // 화살표 버튼 이전 이미지
  const movePrevImg = () => {
    if (carouselIndex === 1) {
      setCarouselIndex(carouselData.length);
    } else {
      setCarouselIndex(carouselIndex - 1);
    }
  };

  return (
    <>
      <S.CarouselLayout>
        {carouselData.map((item, index) => {
          return (
            <Link to="#" key={item.id}>
              <S.ImgBox
                opacity={carouselIndex === index + 1 ? "active" : "none"}
              >
                <img
                  src={process.env.PUBLIC_URL + `/Imgs/image${index + 1}.jpg`}
                  alt={item.alt}
                />
              </S.ImgBox>
            </Link>
          );
        })}
        {/* 캐러셀 화살표 이동 버튼 */}
        <S.ArrowBtn type="button" direct="left" onClick={movePrevImg}>
          <LeftArrow stroke="white" />
        </S.ArrowBtn>
        <S.ArrowBtn type="button" direct="right" onClick={moveNextImg}>
          <RightArrow stroke="white" />
        </S.ArrowBtn>
      </S.CarouselLayout>
    </>
  );
}

export default Carousel;

- 위 코드를 보면 ReactComponent as LeftArror 로 이미지를 불러오는 걸 볼 수 있다.

왜 ReactComponent 를 불러와야할까?

 

ReactComponent 는 SVG 파일을 React 컴포넌트로 가져오는 방법 중 하나이다. 

SVG는 XML 기반의 벡터 그래픽을 나타내는 이미지 형식으로, 다양한 크기에서도 고품질의 이미지를 유지할 수 있다. ReactComponent를 사용하면 SVG 파일을 React 컴포넌트로 가져와서 JSX 코드 내에서 쉽게 사용할 수 있다.
예를들어, 'stroke','fill' 등 SVG 속성을 props로 전달하여 동적으로 변경할 수 있고, 이벤트 핸들러를 추가하여 SVG를 클릭 가능하게 만들 수 있다. 

 

아래 svg 코드 예시

- stroke 를 current 로 해놔야 props로 색상을 받을 수 있다. 

 <LeftArrow stroke="white" /> 로 해놨기에 흰색 색상을 받을 수 있다.

5) 이미지 하단의 닷버튼 만들자

// style.ts
export const DotBox = styled.div`
  position: absolute;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 10px;
`;

export const DotBtn = styled.button<{ isActive: string }>`
  width: 10px;
  height: 10px;
  background-color: ${(props) =>
    props.isActive === "active" ? "#000000" : "#FFFFFF"};
  border-radius: 50%;
`;

코드 완성본

import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import * as S from "./style";
import { carouselData } from "./carouselData";
import { ReactComponent as LeftArrow } from "../../../assets/images/icon-swiper-left.svg";
import { ReactComponent as RightArrow } from "../../../assets/images/icon-swiper-right.svg";

function Carousel() {
  const [carouselIndex, setCarouselIndex] = useState(1);

  useEffect(() => {
    const timer = setInterval(() => {
      moveNextImg();
    }, 5000);

    return () => {
      clearInterval(timer);
    };
  }, [carouselIndex]);

  // 화살표 버튼 이후 이미지
  const moveNextImg = () => {
    if (carouselIndex !== carouselData.length) {
      setCarouselIndex(carouselIndex + 1);
    } else {
      setCarouselIndex(1);
    }
  };
  // 화살표 버튼 이전 이미지
  const movePrevImg = () => {
    if (carouselIndex === 1) {
      setCarouselIndex(carouselData.length);
    } else {
      setCarouselIndex(carouselIndex - 1);
    }
  };
  // 닷 버튼 이동
  const moveDot = (index: number) => {
    setCarouselIndex(index);
  };
  return (
    <>
      <S.CarouselLayout>
        {carouselData.map((item, index) => {
          return (
            <Link to="#" key={item.id}>
              <S.ImgBox
                opacity={carouselIndex === index + 1 ? "active" : "none"}
              >
                <img
                  src={process.env.PUBLIC_URL + `/Imgs/image${index + 1}.jpg`}
                  alt={item.alt}
                />
              </S.ImgBox>
            </Link>
          );
        })}
        {/* 캐러셀 화살표 이동 버튼 */}
        <S.ArrowBtn type="button" direct="left" onClick={movePrevImg}>
          <LeftArrow stroke="white" />
        </S.ArrowBtn>
        <S.ArrowBtn type="button" direct="right" onClick={moveNextImg}>
          <RightArrow stroke="white" />
        </S.ArrowBtn>

        {/* 캐러셀 닷 버튼  */}
        <S.DotBox>
          {Array.from({ length: carouselData.length }).map((_, index) => {
            return (
              <S.DotBtn
                key={index}
                isActive={carouselIndex === index + 1 ? "active" : ""}
                onClick={() => moveDot(index + 1)}
              />
            );
          })}
        </S.DotBox>
      </S.CarouselLayout>
    </>
  );
}

export default Carousel;