Front/next.js

[겨울방학-회사] React로 자동 전환 슬라이드 쇼 만들기 : Carousel 구현

ddo04 2025. 2. 14. 21:14
    • 슬라이드 디자인은 shadcn/ui의 Carousel를 사용하여 구현하였습니다

IntersectionObserver
useEffect 훅을 이용한 초기화 및 정리
useEffect(() => {
	// IntersectionObserver 생성 및 감지 로직
	return () => {
    	// intersectionObserver 감시 해제 로직
    }
  }, []);
  • useEffect 훅은 Reac 컴포넌트가 렌더링될 때 특정 작업을 수행하도록 하는 훅입니다
  • 코드에서 IntersectionObserver를 생성하고 감시를 시작하는 작업을 수행합니다
  • 두 번째 인자로 빈 배열 [ ]을 전달하여, useEffect의 컴포넌트가 마운드될 때 단 한 번만 실행됩니다
  • useEffect의 반환값을 return 함수로 전달하면, 컴포넌트가 언마운트될 때 실행됩니다
  • return 함수 내에서는 IntersectionObserver의 감시를 해제하여 메모리 누수를 방지하고 불필요한 동작을 막습니다
IntersectionObserver 생성 및 설정
const observer = new IntersectionObserver(
    (entries) => {
        if (entries[0].isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
    },
    { threshold: 0.1 }
);
  • new IntersectionObserver(callback, options)를 사용하여 IntersectionObserver 인스턴스를 생성합니다
  • callback 함수
    • IntersectionObserver가 감지하는 요소의 가시성이 변경될 때마다 실행되는 함수입니다
    • entries 배열을 인자로 받고 각 요소의 교차 상태 정보를 담고 있습니다
    • entries[0].isIntersecting은 감지 대상 요소가 뷰 포트와 교차하는 지 여부를 나타내는 불리언 값입니다, true 값이면 교차하고 있다는 의미입니다
    • seIsVisible(true)는 isVisible 상태를 true로 설정합니다, 상태는 다른 곳에서 특정 애니메이션을 트리거하는 데 사용될 것입니다
    • observer.disconnect()는 더 이상 감시할 필요가 없어졌으므로, observer를 해제합니다. 이렇게 하면 불필요한 감시 동작을 방지하여 성능을 최적화할 수 있습니다
  • option 객체
    • IntersectionObserver의 동작 방식을 설정하는 옵션입니다
    • threshold: 0.1는 감지 대상 요소의 10% 이상이 뷰포트에 보여질 때 callback 함수를 실행하도록 설정합니다. threshold 값을 조절하여 감시 시점을 조절할 수 있습니다.
    • 0은 요소가 완전히 가려져 있을 때, 1은 요소가 완전히 보여줄 때를 의미합니다
감시 대상 요소 설정
if (sectionRef.current) {
	observer.observe(sectionRef.current);
}
  • sectionRef는 React의 useRef 훅을 사용하여 생성된 ref 객체입니다
  • ref는 감시 대상 요소에 연결되어 있습니다
  • sectionRef.current는 ref에 연결된 DOM 요소를 가리킵니다
  • observer.observe(sectionRef.current)는 IntersectionObserver에게 sectionRef.current 요소를 감시하도록 지시합니다
정리 함수 (Cleanup Function)
return () => {
    if (observer && sectionRef.current) {
        observer.unobserve(sectionRef.current);
    }
};
  • useEffect의 반환 값으로 함수를 전달하면, 컴포넌트가 언마운트될 때 실행됩니다
  • 코드에서 컴포넌트가 언마운트될 때 observer.unobserve(sectionRef.current)를 호출하여 감시를 해제합니다
  • 메모리 누수를 방지하고, 불필요한 IntersectionObserver 동작을 막을 수 있습니다
Carousel Plugins
Autoplay 플러그인 설정
const plugin = useRef(Autoplay({ delay: 5000, stopOnInteraction: true }));
  • useRef 훅을 사용하여 Autoplay 플러그인 인스턴스를 생성하고, plugin 변수에 저장합니다
  • useRef를 사용하는 이유는 컴포넌트가 다시 렌더링 되어도 Autoplay 인스턴스가 유지되도록 하기 위함입니다
  • Autoplay 플러그인은 embla-carousel의 자동 슬라이드 기능을 제공합니다
  • delay:5000 옵션은 슬라이드 전환 간격을 5초로 설정합니다
  • stopOnInteraction: true 옵션은 사용자가 슬라이드와 상호작용(마우스 호버 등)할 때 자동 슬라이드 재생을 멈추도록 설정합니다
Carousel 컴포넌트 및 플러그인 적용
<Carousel
        plugins={[plugin.current]}
        onMouseEnter={plugin.current.stop}
        onMouseLeave={plugin.current.play}
>
  • Carousel 컴포넌트는 embla-carousel의 핵심 컴포넌트로, 슬라이드 쇼를 구성하는 역할을 합니다
  • plugins prop을 통해 Autoplay 플러그인을 적용하여 자동 슬라이드 기능을 활성화합니다
  • plugin.current를 사용하는 이유는 useRef로 생성한 Autoplay 인스턴스에 접근하기 위함입니다
  • onMouseEnter 이벤트 핸들러는 마우스가 슬라이드 영역에 진입했을 때 plugin.current.stop 메서드를 호출하여 자동 슬라이드 재생을 멈춥니다.
  • onMouseLeave 이벤트 핸들러는 마우스가 슬라이드 영역에서 벗어날을 때 plugin.current.play 메서드를 호출하여 자동 슬라이드 재생을 다시 시작합니다
숫자 카운트 애니메이션
useEffect 훅과 조건부 실행
useEffect(() => {
    if (isVisible) {
        // ... 애니메이션 로직 ...
    }
}, [isVisible]);
  • useEffect 훅은 React 컴포넌트가 렌더링될 때 특정 작업을 수행하도록 하는 훅입니다
  • 코드에서 숫자를 증가시키는 애니메이션을 실행하는 역할을 합니다
  • 두 번째 인자로 [isVisible]을 전달했기 때문에, useEffect는 isVisible 값이 변경될 때마다 실행됩니다
  • if (isVisible) 조건문은 isVisible 값이 true일 때만 애니메이션 로직을 실행하도록 합니다
  • isVisible은 아마도 다른 부분에서 어떤 요소가 화면에 나타나는지 감시하여 설정되는 상태값입니다
  • 특정 요소가 화면에 보여질 때만 숫자가 카운트업 되는 애니메이션이 시작되도록 하는 것입니다
애니메이션 설정
const targetNum = 10000;
const duration = 2000;
const increment = targetNum / (duration / 10);
let currentNum = 0;
  • targetNum은 애니메이션의 최종 목표 숫자 값
  • duration은 애니메이션 지속 시간
  • increment는 각 프레임마다 증가시킬 숫자 값
  • targetNum을 duration 동안 몇 번 증가시킬지 계산하여 결정합니다
  • currentNum은 현재 숫자 값, 애니메이션이 시작될 때 0으로 초기화됩니다
requestAnimationFrame을 이용한 애니메이션 구현
const animateCount = () => {
    if (currentNum < targetNum) {
        currentNum += increment;
        setCount(Math.floor(currentNum));
        requestAnimationFrame(animateCount);
    } else {
        setCount(targetNum);
    }
};
animateCount();
  • animateCount 함수는 실제로 숫자를 증가시키고 화면을 업데이트하는 역할을 합니다
  • if (currentNum < targetNum) 조건문은 현재 숫자가 목표 숫자에 도달할 때까지 애니메이션을 게속합니다
  • currentNum += increment는 현재 숫자를 increment만큼 증가시킵니다
  • setCount(Math.floor(currentNum)은 증가된 숫자를 setCount 함수를 사용하여 상태 값(count)을 업데이트합니다
  • Math.floor()는 소수점 이하를 버림하여 정수만 표시하도록 합니다
  • count 상태 값은 화면에 표시되는 숫자를 나타내는 데 사용될 것입니다
  • requestAnimationFrame(animateCount)는 브라우저에게 다음 프레임에 animateCount 함수를 다시 실행하도록 요청합니다. 이를 통해 부드러운 애니메이션 효과를 구현할 수 있습니다
  • requestAnimationFrame은 브라우저 렌더링 주기에 맞춰 콜백 함수를 실행하므로, 애니메이션 성능을 최적화하는 데 효과적입니다
  • else 블록은 currentNum이 targetNum에 도달했을 때 실행됩니다.
  • setCount(targetNum)을 호출하여 최종 목표 숫자를 정확하게 표시하고 애니메이션을 종료합니다
  • animateCount() 함수를 즉시 호출하여 애니메이션을 시작합니다
전체 코드
import { Card, CardContent } from "../ui/card";
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "../ui/carousel";
import { PROPOSAL_LIST } from "@/app/_consts/proposal_list";
import { useEffect, useRef, useState } from "react";
import Autoplay from "embla-carousel-autoplay";

export function ProposalSection() {
  const [count, setCount] = useState(0);
  const sectionRef = useRef(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { threshold: 0.1 }
    );

    if (sectionRef.current) {
      observer.observe(sectionRef.current);
    }

    return () => {
      if (observer && sectionRef.current) {
        observer.unobserve(sectionRef.current);
      }
    };
  }, []);

  useEffect(() => {
    if (isVisible) {
      const targetNum = 10000;
      const duration = 2000;
      const increment = targetNum / (duration / 10);
      let currentNum = 0;
      const animateCount = () => {
        if (currentNum < targetNum) {
          currentNum += increment;
          setCount(Math.floor(currentNum));
          requestAnimationFrame(animateCount);
        } else {
          setCount(targetNum);
        }
      };
      animateCount();
    }
  }, [isVisible]);

  const plugin = useRef(Autoplay({ delay: 5000, stopOnInteraction: true }));

  return (
    <div ref={sectionRef}>
      <h1 className="font-black text-center text-6xl mt-10 font-serif">
        Heading
      </h1>
      <Carousel
        plugins={[plugin.current]}
        className="w-full max-w-screen-sm p-5"
        onMouseEnter={plugin.current.stop}
        onMouseLeave={plugin.current.play}
      >
        <CarouselContent>
          {PROPOSAL_LIST.map((proposal) => (
            <CarouselItem key={proposal.id}>
              <div className="p-1">
                <Card>
                  <CardContent className="flex aspect-square items-center justify-center">
                    <img
                      src={proposal.image}
                      alt={`${proposal.id}번 프로토절: ${proposal.title || "제목 없음"}`}
                    />
                  </CardContent>
                </Card>
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious className="w-20 h-20 -ml-5" />
        <CarouselNext className="w-20 h-20 -mr-5" />
      </Carousel>
      <div className="w-full max-w-screen-sm">
        <h1 className="font-black text-center text-4xl m-10 font-sans">
          생성한 기획안 수 {count.toLocaleString()}
        </h1>
      </div>
    </div>
  );
}

'Front > next.js' 카테고리의 다른 글

미들웨어란?  (0) 2025.03.23