랜딩페이지(1) - 풀페이지 스크롤 구현

·2024년 10월 14일
0

Handtris-solo

목록 보기
1/1
post-thumbnail

풀페이지 스크롤

개요

팀프로젝트였던 HANDTRIS를 잘 파악하지 못한 것 같아서, 여기서 백엔드 통신 부분을 제외하고, 솔로 플레이 환경에 여러가지 기술을 좀 추가해서 괜찮은 포트폴리오로 쓸 수 있게 리팩토링 에 집중된 포트폴리오로 리팩토링 중입니다.
일단은 랜딩페이지를 구성해야할 것 같아서, 랜딩 페이지를 풀페이지 스크롤 형태로 구성하고 있습니다.


풀페이지 스크롤 구현

먼저 풀페이지 스크롤을 구현하면서 가장 중요시했던거는 '쫀득한' 스크롤 경험입니다.
여기서 말하는 '쫀득함'이란,
스크롤이 조금 움직일때는 스크롤이 되지 않고 스크롤이 어느정도 임계값을 넘었을 때 한 페이지씩 스크롤이 내려가는 형태입니다.

CSS는 tailwindcss를 사용했습니다.

export default function LandingPage() {
  return (
	<div className="h-screen overflow-hidden">
      <Section fromColor="purple-600" toColor="indigo-800" flexDirection="col" />
      <Section fromColor="indigo-800" toColor="blue-600" />
      <Section fromColor="blue-600" toColor="cyan-500" />
      <Section fromColor="teal-400" toColor="green-500" flexDirection="col" />
    </div>
  );
}
  • snap-start는 각 섹션의 시작 부분에 정확히 스크롤이 맞춰지도록 해줍니다.

풀페이지 스크롤 효과를 위해 각 섹션은 h-screen으로 화면을 꽉 채운 뒤 snap-start를 적용하면 간단하게 구성할 수 있습니다.

현재 상황

풀페이지 스크롤


쫀득한 스크롤 효과 추가

섹션이 나뉘기만 하는 것으로는 '쫀득함'이 부족했다. 현재 상태에서는 스크롤을 멈추면 페이지가 전환되지만, 미세하게 스크롤을 움직일 때도 스크롤이 반응하는 상황입니다. 제가 원하는 것은 평소에 스크롤이 반응하지 않으면서, 페이지 전환때만 스크롤이 움직이는 것이다.

이를 위해 섹션별로 sectionRefs배열에 useRef를 (일단은 3페이지니까 3개)저장했다.
useRef를 이용해 섹션을 배열로 관리하고, 현재 섹션의 인덱스를 저장하는 useState 변수를 선언합니다.

const sectionRefs: RefObject<HTMLDivElement>[] = [
    useRef(null),
    useRef(null),
    useRef(null),
];

const [currentSectionIndex, setCurrentSectionIndex] = useState(0);

마우스 휠 이벤트 처리

이제 마우스 휠 이벤트에 따라 적절한 섹션으로 스크롤하는 로직을 추가했습니다. handleWheel 함수에서 스크롤 방향을 감지하고, 일정 시간 동안 추가 스크롤을 차단하여 한 번에 한 섹션만 이동하도록 제어합니다.

const handleWheel = (e: ReactWheelEvent<HTMLDivElement>) => {
    if (isScrolling.current) return;
    isScrolling.current = true;
    setTimeout(() => {
      isScrolling.current = false;
    }, 800);
    if (e.deltaY > 0) {
      scrollToNextSection();
    } else {
      scrollToPrevSection();
    }
  };

1.isScrolling.current를 통해 현재 스크롤 중인지 판단
2. 타임아웃을 800ms 걸어주어서 isScrolling.current를 다시 false로 바꾸어 추가적인 스크롤이 가능하게 만듭니다. 여러 섹션에 대한 이벤트가 한번에 이루어지지 않게 방지하고자 했습니다.
4. e.deltaY 값을 통해 스크롤 방향을 감지 양수면 다음섹션, 음수면 이전섹션으로 이동합니다.

이 방식으로 연속된 스크롤을 제한하고, 한 번에 한 섹션씩만 이동하게 하는 '쫀득한' 스크롤 효과를 구현할 수 있습니다.

스크롤 제어 함수

const scrollToNextSection = () => {
  scrollToSection(currentSectionIndex + 1);
};
const scrollToPrevSection = () => {
  scrollToSection(currentSectionIndex - 1);
};
const scrollToSection = (index: number) => {
  if (index >= 0 && index < sectionRefs.length) {
    setCurrentSectionIndex(index);
    sectionRefs[index].current?.scrollIntoView({ behavior: "smooth" });
  }
};

최상위 divonWheel 이벤트를 추가하고, 섹션마다 ref를 넘겨주면 마우스 휠 이벤트로서 섹션간 이동을 해줄 수 있었다.

현상황

개선전

  • 여전히 그렇게 쎄게 드래그하지도 않았는데 1에서 3페이지로 쭉쭉 넘어가버린다. 한페이지만 넘어가야하는데....

여러 섹션이 한꺼번에 스크롤되는 문제 해결

스크롤을 강하게 할 경우 한 번에 여러 섹션이 이동하는 문제를 해결하기 위해, handleWheel 함수에서 e.deltaY 값의 크기를 확인합니다. 이를 통해 스크롤이 일정 강도를 넘길 때만 섹션 이동을 허용해줄 겁니다.

const handleWheel = (e: ReactWheelEvent<HTMLDivElement>) => {
    if (isScrolling.current) return;
    isScrolling.current = true;
    setTimeout(() => {
      isScrolling.current = false;
    }, 400);
    if (Math.abs(e.deltaY) > 20) {
      console.log("deltaY", e.deltaY);
      console.log("페이지 넘기고 싶구나!");
      if (e.deltaY > 0) {
        scrollToNextSection();
        return;
      } else {
        scrollToPrevSection();
        return;
      }
    }
  };

이렇게 하면 스크롤 강도(e.deltaY)가 20 이상일 때에만 섹션 이동이 일어나고, 너무 약한 스크롤은 무시되게 했습니다.
그리고 그 스크롤이 높은 값까지 올라왔다가 순차적으로 다시 0으로 수렴하는 작동방식을 띄기 때문에 20이상일때 작동하고 페이지를 옮겨준다음 return;해서 처리를 해줬습니다.
setTimeout의 시간을 너무 적게하면 연속으로 스크롤이 적용되다보니까, 사용자가 불편함을 느끼지 않을정도로 400ms정도로 설정을 했다.


최종 결과

최종

  • 한슬라이드씩 잘 움직인다. 중간에 스크롤이 멈추지 않는다. -> '쫀득'하게 보이는 것 같다.
    하지만 아직은 좀 아쉬운 부분이 있는데,
    다음에는 이제 Fullpage.js예시 처럼 스크롤을 여러번해서 어느정도의 임계값에 도달하면 넘겨지는것으로 구현을 해야겠다.

코드

LANDINGPAGE

"use client";
import {
  useRef,
  useState,
  RefObject,
  WheelEvent as ReactWheelEvent,
} from "react";
import Section from "@/components/layout/Section";
import { ArrowDownCircle } from "lucide-react";

export default function LandingPage() {
  const sectionRefs: RefObject<HTMLDivElement>[] = [
    useRef(null),
    useRef(null),
    useRef(null),
  ];
  const [currentSectionIndex, setCurrentSectionIndex] = useState(0);
  const isScrolling = useRef(false);

  const handleWheel = (e: ReactWheelEvent<HTMLDivElement>) => {
    if (isScrolling.current) return;
    isScrolling.current = true;
    setTimeout(() => {
      isScrolling.current = false;
    }, 400);
    if (Math.abs(e.deltaY) > 20) {
      console.log("deltaY", e.deltaY);
      console.log("페이지 넘기고 싶구나!");
      if (e.deltaY > 0) {
        scrollToNextSection();
        return;
      } else {
        scrollToPrevSection();
        return;
      }
    }
  };

  const scrollToNextSection = () => {
    scrollToSection(currentSectionIndex + 1);
  };

  const scrollToPrevSection = () => {
    scrollToSection(currentSectionIndex - 1);
  };

  const scrollToSection = (index: number) => {
    if (index >= 0 && index < sectionRefs.length) {
      setCurrentSectionIndex(index);
      sectionRefs[index].current?.scrollIntoView({ behavior: "smooth" });
    }
  };

  return (
    <div onWheel={handleWheel} className="h-screen overflow-hidden">
      <Section
        ref={sectionRefs[0]}
        fromColorClass="bg-purple-600"
        toColorClass="bg-indigo-800"
        flexDirection="flex-col"
      >
        <h1 className="text-4xl font-bold text-white mb-8">Welcome</h1>
        <button
          onClick={scrollToNextSection}
          className="bg-white text-purple-600 px-6 py-3 rounded-full font-semibold hover:bg-opacity-90 transition-all flex items-center"
        >
          Learn More
          <ArrowDownCircle className="w-6 h-6 ml-2 inline animate-bounce" />
        </button>
      </Section>
      <Section
        ref={sectionRefs[1]}
        fromColorClass="bg-teal-400"
        toColorClass="bg-green-500"
        flexDirection="flex-col"
      >
        <h2 className="text-4xl font-bold text-white mb-8">Game Start</h2>
      </Section>
      <Section
        ref={sectionRefs[2]}
        fromColorClass="bg-red-500"
        toColorClass="bg-orange-500"
        flexDirection="flex-col"
      >
        <h2 className="text-4xl font-bold text-white mb-8">Game End</h2>
      </Section>
    </div>
  );
}

SECTION

export default function Section({
  children,
  fromColorClass,
  toColorClass,
  flexDirection = "flex-row",
  ref,
}: {
  children: React.ReactNode;
  fromColorClass: string;
  toColorClass: string;
  flexDirection?: "flex-row" | "flex-col";
  ref: React.LegacyRef<HTMLDivElement>;
}) {
  return (
    <div
      ref={ref}
      className={`bg-gradient-to-b ${fromColorClass} ${toColorClass} flex ${flexDirection} items-center justify-center h-screen snap-start`}
    >
      {children}
    </div>
  );
}
profile
기억보단 기록을

0개의 댓글