본문 바로가기
Next.js/개발 노트

[React/Next.js] 라이브러리 없이 구현하는 3D carousel

by tokkiC 2023. 12. 31.

react에서 css와 html 만으로 구현하였다


라이브러리를 사용하지 않고 3D carousel 구현

carousel 은 라이브러리로 구현하면 사용하기 편리하지만, 이 구현을 한 포트폴리오 프로젝트는 최소한의 라이브러리를 사용해서 직접 구현하고자 정했기에, next.js와 css와 html 만으로 구현하였다.
2d 형식의 슬라이드는 너무 못생겼고 간단했기에, 3d로 구현해보았다.

핵심 원리를 요약하자면,

  1. Transform 속성 활용: 각 캐러셀 아이템에 transform CSS 속성을 사용하여 3D 회전(rotateY)과 깊이(translateZ)를 조정해 입체적인 효과를 구현
  2. 조건부 렌더링과 인덱스 관리: active 상태 변수를 사용하여 현재 활성화된 캐러셀 아이템을 추적하고, 이를 바탕으로 각 아이템의 위치와 스타일을 동적으로 변경
  3. 사용자 상호작용: 화살표 버튼을 클릭하면 setActive 함수를 통해 active 상태가 업데이트되며, 이는 캐러셀 아이템의 위치와 모양을 변경하여 사용자에게 상호작용 가능한 인터페이스를 제공

이렇게 된다.

아래의 코드의 각 줄마다 상세 설명을 주석으로 남겨두었다.

Next.js 14와 타입스크립트를 사용하였지만, React에도 "use client" 를 제거하면 그대로 사용 가능하다.

"use client";

import React from "react";
import { useState } from "react";
import { IoIosArrowBack, IoIosArrowForward } from "react-icons/io";

export default function Carousel({ children }: { children: React.ReactNode }) {
  const [active, setActive] = useState(0);
  const count = React.Children.count(children);

  return (
    <div
      className="relative w-full h-[17rem] overflow-hidden mt-2 mb-4 select-none"
      style={{
        perspective: `600px`,
        transformStyle: `preserve-3d`,
      }}
    >
      {active > 0 && (
        <button
          className="absolute top-1/2 -translate-y-1/2 z-30 cursor-pointer select-none"
          style={{
            transform: `translateX(130%) translatey(-80%)`,
          }}
          onClick={() => setActive((i) => i - 1)}
        >
          <IoIosArrowBack className="hover:text-yellow-200 text-6xl" />
        </button>
      )}
      {React.Children.map(children, (child, i) => {
        const isActive = i === active;
        return (
          <div
            className="absolute w-full h-full transition-all duration-300 ease-out select-none"
            style={{
              transform: `rotateY(${(active - i) * 30}deg) scaleY(${
                1 + Math.abs(active - i) * -0.2
              }) translateZ(${Math.abs(active - i) * -30}rem) translateX(${
                Math.sign(active - i) * -3
              }rem)`,
              filter: `blur(${Math.abs(active - i)}rem)`,
              zIndex: isActive ? 25 : 20 - Math.abs(active - i),
            }}
          >
            {child}
          </div>
        );
      })}
      {active < count - 1 && (
        <button
          className="absolute top-1/2 -translate-y-1/2 right-0 z-30 cursor-pointer select-none"
          style={{
            transform: `translateX(-130%) translatey(-80%)`,
          }}
          onClick={() => setActive((i) => i + 1)}
        >
          <IoIosArrowForward className="hover:text-yellow-200 text-6xl" />
        </button>
      )}
    </div>
  );
}
import Carousel from "../Carousel";
import PersonalProjectCard from "../PersonalProjectCard/PersonalProjectCard";
import ProjectCard from "../ProjectCard/ProjectCard";
import { mainProjectsData, personalSideProjectsData } from "./projectsData";
import style from "./Projects.module.css";

export default function Projects() {
  return (
    <section className={`${style.container}`}>
      <div className="scroll-point h-12 w-full" id="projects"></div>

      <div>
        {/* mt-12는 네비게이션바의 높이. h2가 가려지지 않도록 설정함 */}
        <h2 className={`${style.head2}`}>Projects</h2>
        {/* 데이터 가져와서 map 으로 그리기.map의 index를 사용해서 index % 2 ? RightProjectCard : LeftProjectCard 로 그리기 */}
        {mainProjectsData.map((project, index) => (
          <ProjectCard
            key={index}
            projectName={project.projectName}
            imageUrl={project.imageUrl}
            techs={project.techs}
            description={project.description}
            githubCodeUrl={project.githubCodeUrl}
            deploymentUrl={project.deploymentUrl}
            reverse={index % 2 !== 0}
            myWork={project.myWork}
          />
        ))}
      </div>

      <p className={`${style.toyProjects}`}>Toy Projects</p>
      <Carousel>
        {personalSideProjectsData.map((project, index) => (
          <PersonalProjectCard
            key={index}
            projectName={project.projectName}
            imageUrl={project.imageUrl}
            description={project.description}
            githubCodeUrl={project.githubCodeUrl}
            myWork={project.myWork}
            techs={project.techs}
            deploymentUrl={project.deploymentUrl}
          />
        ))}
      </Carousel>
    </section>
  );
}
.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  margin-bottom: 2rem;
}

.head2 {
  margin-bottom: 2rem;
  line-height: 1;
}

.toyProjects {
  font-size: 2rem;
  font-weight: 600;
  text-align: center;
  margin-bottom: 1rem;
}

@media screen and (min-width: 450px) {
  .head2 {
    margin-bottom: 3rem;
  }
}

@media screen and (min-width: 1024px) {
  .container {
    margin-bottom: 4rem;
  }

  .head2 {
    margin-bottom: 4rem;
  }
}

댓글