7/24 TIL (최종 프로젝트 2번째 섹션 애니메이션 구현)

Hwi·2024년 7월 24일

TIL

목록 보기
77/96

1 > 2번 세션으로 넘어갈 때, 2번 세션이 1번 세션을 서서히 덮는 듯한 모션을 구현하기 위해 useEffect 코드를 수정했습니다.

useEffect(() => {
    sectionsRef.current.forEach((section) => {
      if (section) {
        ScrollTrigger.create({
          trigger: section, // 세션을 트리거로 설정
          start: 'top top', // 세션 시작 부분이 뷰포트 상단에 도달할 때 애니메이션 시작
          pin: true, // 세션을 고정시켜 스크롤할 때 위치 고정
          pinSpacing: false, // 세션 사이 여백을 제거 
          scrub: true, // 부드럽게 연결
        });
      }
    });
  }, []);

이렇게 해주니 원하는 대로 구현은 잘 됐으나 문제점을 하나 발견했습니다.

바로 페이지가 처음 렌더링 됐을 시에, 혹은 새로고침을 했을 때 스크롤을 내리기 전엔 백그라운드에 설정해둔 배경이 안 나온다는 것인데요.

이를 해결하기 위해 열심히 서칭을 해본 결과

HTMLElement 인터페이스에 속해있는 readyState 속성을 사용해서 해결할 수 있었습니다.

readyState 속성은 비디오 혹은 오디오 요소의 현재 준비 상태를 나타내는 값입니다.

readyState 속성은 아래와 같은 값들을 가집니다.

0 (HAVE_NOTHING): 미니어의 정보가 전혀 제공되지 않았음(사용 불가)
1 (HAVE_METADATA): 메타데이터는 사용 가능하지만, 실제 프레임은 사용 불가
2 (HAVE_CURRENT_DATA): 현재 재생 위치에 해당하는 데이터는 사용할 수 있지만, 그 다음 프레임 데이터는 준비 X
3 (HAVE_FUTURE_DATA): 현재 재생 위치와 그 다음 프레임에 해당하는 데이터를 사용할 수 있고, 재생이 원할하게 이루어짐
4 (HAVA_ENOUGH_DATA): 미디어의 충분한 데이터가 로드됨

수정 후 코드
비디오가 로드된 후에 ScrollTrigger를 설정해야 하므로, useState와 useEffect를 사용해 비디오가 로드됐는지 확인을 먼저 한 후에, videoLoaded 상태가 true일 때만 ScrollTrigger를 설정하여 비디오가 정상 로드된 후에야 애니메이션 효과가 정상적으로 작동하도록 수정했습니다.

'use client';
import Header from '@/components/common/Header';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/all';
import React, { useEffect, useRef, useState } from 'react';

gsap.registerPlugin(ScrollTrigger);

const MainPage = () => {
  const sectionsRef = useRef<(HTMLElement | null)[]>([]);
  const videoRef = useRef<HTMLVideoElement | null>(null);
  const [videoLoaded, setVideoLoaded] = useState(false);

  useEffect(() => {
    if (videoRef.current) {
      const videoElement = videoRef.current;
      const checkVideoLoaded = () => {
        if (videoElement.readyState >= 3) { // 
          setVideoLoaded(true);
        }
      };
      checkVideoLoaded();
    }
  }, []);

  useEffect(() => {
    if (videoLoaded) {
      sectionsRef.current.forEach((section) => {
        if (section) {
          ScrollTrigger.create({
            trigger: section,
            start: 'top top',
            pin: true,
            pinSpacing: false,
            scrub: true,
          });
        }
      });
      ScrollTrigger.refresh();
    }
  }, [videoLoaded]);

  return (
    <div className="w-full">
      <Header />
      <section
        ref={(el) => {
          sectionsRef.current[0] = el;
        }}
        className="section h-screen flex items-center justify-center relative"
      >
        <video
          ref={videoRef}
          className="absolute top-0 left-0 w-full h-full object-cover z-0"
          src="/videos/우주.mp4"
          autoPlay
          loop
          muted
        />
        <div className="absolute z-10 text-center top-48 sm:w-auto sm:text-left sm:left-48 md:left-40 lg:left-52 xl:left-64">
          <h1 className="text-white text-6xl font-bold text-purple-200">Voyage X</h1>
          <p className="text-white p-4 text-3xl">상상을 현실로, 우주에서의 만남</p>
          <p className="text-white p-4">
            우주 여행의 문을 여는 창구, Voyage X입니다.
            <br />
            상상으로 꿈꾸던 우주 여행을 현실로 만들어 드립니다.
          </p>
        </div>
      </section>

      <section
        ref={(el) => {
          sectionsRef.current[1] = el;
        }}
        className="section h-screen flex items-center justify-center relative"
      >
        <div className="scroll-container h-full w-full">
          <div className="scroll-item bg-gray-300 text-white">
            <h2>Planet 1</h2>
          </div>
          <div className="scroll-item bg-gray-300 text-white">
            <h2>Planet 2</h2>
          </div>
          <div className="scroll-item bg-gray-300 text-white">
            <h2>Planet 3</h2>
          </div>
          <div className="scroll-item bg-gray-300 text-white">
            <h2>Planet 4</h2>
          </div>
          <div className="scroll-item bg-gray-300 text-white">
            <h2>Planet 5</h2>
          </div>
          <div className="scroll-item bg-gray-300 text-white">
            <h2>Planet 6</h2>
          </div>
        </div>
      </section>
    </div>
  );
};

export default MainPage;

후에 정상 작동 되는 것을 확인한 후, 2번째 섹션에서 행성을 슬라이드하는 애니메이션을 추가했습니다.

const [currentSlide, setCurrentSlide] = useState(0);

 useEffect(() => {
    planetsRef.current.forEach((planet, index) => {
      if (planet) {
        const isActive = index === currentSlide;
        const xPos = (index - currentSlide) * 300; // 행성 위치
        const scale = isActive ? 1.5 : 1;
        const zIndex = isActive ? 10 : 0;
        const opacity = isActive ? 1 : 0.5;

        gsap.to(planet, {
          x: xPos,
          scale: scale,
          zIndex: zIndex,
          opacity: opacity,
          duration: 1,
          ease: 'power2.inOut',
        });
      }
    });
  }, [currentSlide]);

결과물 gif 올리고 싶은데 이미지 업로드 실패..

gsap 라이브러리를 이용해 행성의 위치 및 크기 조정, z-index를 조정하여 입체감을 주었습니다.

profile
개발자가 되고 싶어~~~

2개의 댓글

comment-user-thumbnail
2024년 7월 25일

와 대박이네요

1개의 답글