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
내가 구현하고 싶은 기능이 구현되어있는 제일 유명한 라이브러리.
그러나
라이센스를 받아야만 쓸 수있는 제약 때문에 pass
2. react-fullpage 라이브러리 사용
https://www.npmjs.com/package/react-fullpage
그 다음으로 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초마다 이벤트가 발생하도록 최적화.
그러나 최적화 후에 스크롤을 왔다갔다 빠르게 움직이면 간헐적으로 중간에 멈추는 문제가 발생... 아직 이 부분은 수정 중
이렇게 여러 가지 방법을 시도(삽질)한 걸 공유용으로 남김.
비슷한 문제를 겪고 있다면 이 과정이 도움이 되길 바람...
'PROJECT' 카테고리의 다른 글
[웹 포트폴리오 만들기] 6편 - 모바일 반응형 구현과 ..최종 완성 (0) | 2024.05.31 |
---|---|
[웹 포트폴리오 만들기] 5편 - next로 모달 라우팅 구현하기 (0) | 2024.05.21 |
[웹 포트폴리오 만들기] 3편 - 디자인 갈아엎기 (0) | 2024.05.07 |
[웹 포트폴리오 만들기] 2편 - NEXT 로딩화면 구현하기 (0) | 2024.04.23 |
[웹 포트폴리오 만들기] 1편 - 디자인 구상하기 (1) | 2024.04.12 |