본문 바로가기
PROJECT

[웹 포트폴리오 만들기] 4편 - fullpage 구현하기

by 3급우사기 2024. 5. 20.

2024.04.12 - [PROJECT] - [웹 포트폴리오 만들기] 1편 - 디자인 구상하기

2024.04.23 - [PROJECT] - [웹 포트폴리오 만들기] 2편 - NEXT 로딩화면 구현하기

2024.05.07 - [PROJECT] - [웹 포트폴리오 만들기] 3편 - 디자인 갈아엎기

2024.05.21 - [PROJECT] - [웹 포트폴리오 만들기] 5편 - next로 모달 라우팅 구현하기

2024.05.31 - [PROJECT] - [웹 포트폴리오 만들기] 6편 - 모바일 반응형 구현과 ..최종 완성

이 과정을 거치면서 느낀 내 상태

 

웹사이트를 스크롤할 때 섹션에 딱 맞게 스크롤 되는 기능을 fullpage, onepage라고 한다.

이 기능을 포트폴리오 사이트에 구현하기 위해 내가 지나온 과정들을 써본다..

(ㄹㅇ 삽질 엄청 함)

 

1. fullpagejs라이브러리 사용

https://github.com/alvarotrigo/fullPage.js/blob/master/lang/korean/README.md

 

fullPage.js/lang/korean/README.md at master · alvarotrigo/fullPage.js

fullPage plugin by Alvaro Trigo. Create full screen pages fast and simple - alvarotrigo/fullPage.js

github.com

내가 구현하고 싶은 기능이 구현되어있는 제일 유명한 라이브러리.

그러나

라이센스를 받아야만 쓸 수있는 제약 때문에 pass

 

2. react-fullpage 라이브러리 사용

https://www.npmjs.com/package/react-fullpage

 

react-fullpage

An implementation of fullpage.js in react based on react-fullpage. Latest version: 0.1.19, last published: 6 years ago. Start using react-fullpage in your project by running `npm i react-fullpage`. There are 3 other projects in the npm registry using react

www.npmjs.com

 

그 다음으로 React-Fullpage 라이브러리를 사용해 봄. 이 라이브러리는 Fullpage.js와 비슷한 기능을 제공하면서 무료로 사용할 수있음. 하지만 오래된 버전이라 그런지 자꾸 콘솔창에 경고 문구가 뜨고,  나중에 구현하게 된 프로젝트 자세히 보기 창(모달)과 스크롤 기능이 충돌하는 문제가 발생함.

모달이 켜지면 페이지 스크롤을 막고 모달 내부에서만 스크롤이 가능하게 하고 싶었지만, React-Fullpage 라이브러리로는 불가능.

 

3. scroll - snap사용 

function Home() {
  return (
    <div className={styles.scrollContainer}>
      <div className={styles.scrollArea}>
        <Main />
      </div>
      <div className={styles.scrollArea}>
        <About />
      </div>
      <div className={styles.scrollArea}>
        <Projects />
      </div>
      <div className={styles.scrollArea}>
        <Contact />
      </div>
    </div>
  );
}

export default Home;
/* 부모 스크롤 스냅 컨테이너 */
.scrollContainer {
  width: 100vw;
  height: 100vh;
  overflow: auto;
  scroll-snap-type: y mandatory; /* y 축 방향으로만 scroll snap 적용 */
}

/* 자식 스크롤 스냅 영역 */
.scrollArea {
  scroll-snap-align: start; /* 스크롤 위치 맞춤 */
}

 

그 다음으로 CSS에서 기본으로 지원하는 Scroll Snap을 사용해 구현해 봄. 겉보기에는 잘 작동했고, 모달 창이 켜지면 스크롤을 막는 것도 가능했음. 그러나 브라우저 별로 애니메이션 지원이 달라서 문제가 발생함.

사이드 메뉴를 통한 이동이 제대로 작동하지 않았고, 브라우저마다 애니메이션이 다르게 동작해 결국 이 방법도 포기.

 

4. fullpage 내가 구현

결국 chatgpt의 도움을 받아서 fullpage기능을 직접 코드로 구현함..

드디어 완벽하게 작동

감격 눈물

고맙다 샘알트만아.

 

"use client";
import React, { useEffect, useRef, useState } from "react";
import Main from "../../components/main";
import styles from "../../styles/home.module.css";
import About from "../../components/about";
import Projects from "../../components/projects";
import Contact from "../../components/contact";

function Home() {
  const sectionRefs: React.RefObject<HTMLDivElement>[] = [
    useRef<HTMLDivElement>(null),
    useRef<HTMLDivElement>(null),
    useRef<HTMLDivElement>(null),
    useRef<HTMLDivElement>(null),
  ];
  const [isModalOpen, setIsModalOpen] = useState(false);
  const sectionName = ["main", "about", "projects", "contact"];
  useEffect(() => {
    const handleScroll = (e: WheelEvent) => {
      if (isModalOpen) {
        // 모달이 열려 있을 때는 스크롤 이벤트를 처리하지 않습니다.
        return;
      }
      e.preventDefault();

      const { deltaY } = e;
      let currentSectionIndex = sectionRefs.findIndex((ref) => {
        if (ref.current) {
          const { top, bottom } = ref.current.getBoundingClientRect();
          return top <= 0 && bottom > 0;
        }
        return false;
      });

      if (deltaY > 0 && currentSectionIndex < sectionRefs.length - 1) {
        // Scroll down
        const nextSection = sectionRefs[currentSectionIndex + 1];
        if (nextSection.current) {
          nextSection.current.scrollIntoView({
            behavior: "smooth",
            block: "start",
          });
        }
        changeUrlHash(`${sectionName[currentSectionIndex + 1]}`);
      } else if (deltaY < 0 && currentSectionIndex > 0) {
        // Scroll up
        const prevSection = sectionRefs[currentSectionIndex - 1];
        if (prevSection.current) {
          prevSection.current.scrollIntoView({
            behavior: "smooth",
            block: "start",
          });
        }
        changeUrlHash(`${sectionName[currentSectionIndex - 1]}`);
      }
    };

    window.addEventListener("wheel", handleScroll, { passive: false });
    return () => window.removeEventListener("wheel", handleScroll);
  }, [isModalOpen]);
  // URL 해시 변경 함수
  const changeUrlHash = (hash: string) => {
    window.history.pushState({}, "", `#${hash}`);
    window.dispatchEvent(new HashChangeEvent("hashchange"));
  };
  return (
    <div>
      <div ref={sectionRefs[0]} id="main">
        <Main />
      </div>
      <div ref={sectionRefs[1]} id="about">
        <About />
      </div>
      <div ref={sectionRefs[2]} id="projects">
        <Projects isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
      </div>
      <div ref={sectionRefs[3]} id="contact">
        <Contact />
      </div>
    </div>
  );
}

export default Home;

 

5. 쓰로틀링 최적화

순식간에...

그런데 콘솔 로그를 보니 스크롤 이벤트가 조금만 발생해도 수십 수백 개의 이벤트가 발생하는 것을 발견.

스로틀링(Throttling)
일정한 시간 간격(예: 100ms)으로 스크롤 이벤트를 처리합니다. 스로틀링은 일정 시간 동안 이벤트가 발생하면 처음 한 번만 이벤트를 실행하고, 그 후 일정 시간이 지난 후에 다시 이벤트를 실행합니다. 이를 통해 스크롤 이벤트가 빈번하게 발생할 때 이벤트 핸들러의 실행을 제어할 수 있습니다.

 

"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import Main from "../../components/main";
import styles from "../../styles/home.module.css";
import About from "../../components/about";
import Projects from "../../components/projects";
import Contact from "../../components/contact";
import { throttle } from "lodash";
import { usePathname } from "next/navigation";
const Home = () => {
  const sectionRefs = [
    useRef<HTMLDivElement>(null),
    useRef<HTMLDivElement>(null),
    useRef<HTMLDivElement>(null),
    useRef<HTMLDivElement>(null),
  ];

  const sectionNames = ["main", "about", "projects", "contact"];
  const pathname = usePathname();
  const changeUrlHash = useCallback((hash: string) => {
    window.history.pushState({}, "", `#${hash}`);
    window.dispatchEvent(new HashChangeEvent("hashchange"));
  }, []);

  const handleScroll = throttle((e: WheelEvent) => {
    if (pathname.startsWith("/detail")) {
      return;
    }
    e.preventDefault();
    console.log("scroll");
    const { deltaY } = e;
    const currentSectionIndex = sectionRefs.findIndex((ref) => {
      if (ref.current) {
        const { top, bottom } = ref.current.getBoundingClientRect();
        return top <= 0 && bottom > 0;
      }
      return false;
    });

    if (deltaY > 0 && currentSectionIndex < sectionRefs.length - 1) {
      const nextSection = sectionRefs[currentSectionIndex + 1];
      if (nextSection.current) {
        nextSection.current.scrollIntoView({
          behavior: "smooth",
          block: "start",
        });
        changeUrlHash(sectionNames[currentSectionIndex + 1]);
      }
    } else if (deltaY < 0 && currentSectionIndex > 0) {
      const prevSection = sectionRefs[currentSectionIndex - 1];
      if (prevSection.current) {
        prevSection.current.scrollIntoView({
          behavior: "smooth",
          block: "start",
        });
        changeUrlHash(sectionNames[currentSectionIndex - 1]);
      }
    }
  }, 300); // 0.3초 간격으로 이벤트 실행

  useEffect(() => {
    window.addEventListener("wheel", handleScroll, { passive: false });
    return () => window.removeEventListener("wheel", handleScroll);
  }, [handleScroll]);

  return (
    <div>
      {sectionNames.map((name, index) => (
        <div ref={sectionRefs[index]} id={name} key={name}>
          {index === 0 && <Main />}
          {index === 1 && <About />}
          {index === 2 && <Projects />}
          {index === 3 && <Contact />}
        </div>
      ))}
    </div>
  );
};

export default Home;

 

이를 해결하기 위해 Lodash라이브러리로 스로틀링을 적용해 0.3초마다 이벤트가 발생하도록 최적화.

그러나 최적화 후에 스크롤을 왔다갔다 빠르게 움직이면 간헐적으로 중간에 멈추는 문제가 발생... 아직 이 부분은 수정 중

 

이렇게 여러 가지 방법을 시도(삽질)한 걸 공유용으로 남김. 

비슷한 문제를 겪고 있다면 이 과정이 도움이 되길 바람...