현재 포트폴리오에도 필요한 내용은 전부 들어가 있지만, 어딘지 밋밋해 보여서 스크롤을 내리면 컴포넌트 내용이 나타나는 애니메이션을 추가하기로 했다.
전체 컴포넌트를 관리하고 있는 App.js 파일에서 작업을 진행했다.
✨ 화면에 보이는 컴포넌트들의 state 정의
App.js
const [visibleSections, setVisibleSections] = useState([]);
스크롤을 하면 App 내부에 import 된 컴포넌트가 하나씩 화면에 나타나야 한다.
visibleSections는 '화면에 보이는 컴포넌트'들의 id를 저장하는 배열이다.
컴포넌트가 보여야 할 때 visibleSections에 컴포넌트 id를 넣어주어 화면에 컴포넌트를 띄워주도록 했다.
✨ useEffect Hook을 이용한 스크롤 애니메이션 구현
useEffect(() => {
// 스크롤 이벤트 처리 함수
const handleScroll = () => {
// 화면에 스크롤을 통해 나타낼 section들을 저장한 배열
// ref:해당 section의 useRef 객체, id:section을 식별하는 문자열
const sections = [
{ ref: ArchivingRef, id: "archiving" },
{ ref: SkillsRef, id: "skills" },
{ ref: ProjectRef, id: "project" },
{ ref: EduRef, id: "edu" },
{ ref: ContactRef, id: "contact" },
];
// 현재 화면에 보이는 section들의 id를 추적하기 위해 sections 배열을 필터링
// 각 section의 위치 정보를 이용해 현재 보이는 section들을 찾아낸다.
const visibleSections = sections
.filter(({ ref }) => {
const rect = ref.current.getBoundingClientRect();
return (
rect.top <= window.innerHeight / 2 &&
rect.bottom >= window.innerHeight / 2
);
})
.map(({ id }) => id);
// visibleSections 상태를 업데이트해 현재 화면에 보이는 section들의 id를 저장
setVisibleSections(visibleSections);
};
// 스크롤 이벤트 발생할 때마다 handleScroll 함수가 호출되도록 이벤트 리스너 추가
window.addEventListener("scroll", handleScroll);
// useEffecnt의 cleanup 함수 역할.
// 컴포넌트 언마운트 시 이벤트 리스너 정리를 통해 메모리 누수 방지
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
useEffect Hook을 이용해 스크롤 이벤트를 처리하고, 현재 보이는 section(컴포넌트)을 추적해 상태를 업데이트하도록 하였다.
window.addEventListener("scroll", handleScroll); 에 의해 스크롤 이벤트에 대한 이벤트 리스너가 등록되어 있기 때문에 (이벤스 리스너는 한번 등록되면 컴포넌트가 언마운트 되지 않는 이상 계속 작동한다), useEffect 내부 코드는 프로그램 마운트 당시 초기 한번만 실행하면 된다.
🔎 useEffect 내부 코드 뜯어보기
useEffect 내부 코드 중, 화면에 보이는 section의 id를 추적하는 코드 부분을 자세히 살펴보자.
// 현재 화면에 보이는 section들의 id를 추적하기 위해 sections 배열을 필터링
// 각 section의 위치 정보를 이용해 현재 보이는 section들을 찾아낸다.
const visibleSections = sections
.filter(({ ref }) => {
const rect = ref.current.getBoundingClientRect();
return (
rect.top <= window.innerHeight / 2 &&
rect.bottom >= window.innerHeight / 2
);
})
.map(({ id }) => id);
1. 전체 골격
const visibleSections = sections.filter(({ ref }) => {...}).map(({ id }) => id);
sections 배열을 필터링해 현재 화면에 보이고 있는 section들의 id를 추적하는 코드이다.
2. filter
sections.filter(({ ref }) => {...})
sectinos 배열을 순회하면서, 각 section의 ref 속성을 통해 실제 DOM 요소에 대한 참조를 얻어온다.
3. 위치 정보 가져오기
const rect = ref.current.getBoundingClientRect();
각 section의 DOM 요소의 위치 정보를 가져오기 위해 getBoundingClientRect()를 이용한다.
Element.getBoundingClientRect()은 DOM 요소의 크기와 뷰포트에 상대적인 위치 정보를 제공하는 DOMRect 객체를 반환하는 메서드로, 이 메서드를 사용하면 원하는 요소의 위치값을 알아낼 수 있다.
추가로 DOMRect 객체는 다음과 같은 속성을 포함한다.
- x: 요소의 왼쪽 상단 모서리의 x 좌표
- y: 요소의 왼쪽 상단 모서리의 y 좌표
- width: 요소의 너비
- height: 요소의 높이
- top: 요소의 상단 모서리의 y 좌표
- right: 요소의 우측 모서리의 x 좌표
- bottom: 요소의 하단 모서리의 y 좌표
- left: 요소의 왼쪽 모서리의 x 좌표
이 정보를 사용하면 요소의 화면 위치를 기반으로 추가적인 계산이나 코드 구현을 진행할 수 있다.
위의 코드에서는 getBoundingClientRect()를 사용하여 각 section의 위치 정보를 가져와 현재 보이는 section을 판별하고 있다.
4. 보이는 section의 범위 지정
return (rect.top <= window.innerHeight / 2 && rect.bottom >= window.innerHeight / 2);
section의 화면 상단과 하단이 현재 윈도우 화면의 중간 부분에 걸쳐 있으면 해당 section을 화면에 보이는 section으로 간주한다.
** window.innerHeight에 대한 설명참조
5. 보이는 section의 id만을 이용해 새로운 배열 생성
.map(({ id }) => id)
filter를 통해 현재 화면에 보이는 section들을 모두 추출한 후, map을 이용해 각 section의 id만을 새로운 배열에 매핑한다.
😀 결과적으로 const visibleSections에는 현재 화면에 보이는 section들의 id가 담긴 배열이 저장된다.
✨ return문: 컴포넌트 ClassName 동적 변경
return (
<div className="App">
<Header
...
/>
<Home ref={HomeRef} moveToArc={moveToArcHandler} />
<div
ref={ArchivingRef}
className={`section ${
visibleSections.includes("archiving") && "visible"
}`}
>
<Archiving />
</div>
<div
ref={SkillsRef}
className={`section ${visibleSections.includes("skills") && "visible"}`}
>
<Skills />
</div>
<div
ref={ProjectRef}
className={`section ${
visibleSections.includes("project") && "visible"
}`}
>
<Project />
</div>
<div
ref={EduRef}
className={`section ${visibleSections.includes("edu") && "visible"}`}
>
<Edu />
</div>
<div
ref={ContactRef}
className={`section ${
visibleSections.includes("contact") && "visible"
}`}
>
<Contact />
</div>
</div>
);
화면에 나타낼 컴포넌트들을 div로 감싼 뒤, `visibleSections`(화면에 보이는 section의 id가 저장된 배열)에 id가 포함되어 있는지 여부에 따라 visible 클래스를 동적으로 추가하여 section을 화면에 보여준다.
App.css
.section {
opacity: 0;
transform: translateY(20px);
transition: opacity 1s ease, transform 1s ease;
}
.section.visible {
opacity: 1;
transform: translateY(0);
}
전체 코드
App.js
import Home from "./pages/Home";
import Archiving from "./pages/Archiving";
import Skills from "./pages/Skills";
import "./App.css";
import Project from "./pages/Project";
import Edu from "./pages/Edu";
import Contact from "./pages/Contact";
import Header from "./components/Header";
import { useEffect, useRef, useState } from "react";
function App() {
const HomeRef = useRef(null);
const ArchivingRef = useRef(null);
const SkillsRef = useRef(null);
const ProjectRef = useRef(null);
const EduRef = useRef(null);
const ContactRef = useRef(null);
const [visibleSections, setVisibleSections] = useState([]);
const moveToHomeHandler = () => {
HomeRef.current.scrollIntoView({ behavior: "smooth" });
};
const moveToArcHandler = () => {
ArchivingRef.current.scrollIntoView({ behavior: "smooth" });
};
const moveToSkillsHandler = () => {
SkillsRef.current.scrollIntoView({ behavior: "smooth" });
};
const moveToProjectHandler = () => {
ProjectRef.current.scrollIntoView({ behavior: "smooth" });
};
const moveToEduHandler = () => {
EduRef.current.scrollIntoView({ behavior: "smooth" });
};
const moveToContactHandler = () => {
ContactRef.current.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
const handleScroll = () => {
const sections = [
{ ref: ArchivingRef, id: "archiving" },
{ ref: SkillsRef, id: "skills" },
{ ref: ProjectRef, id: "project" },
{ ref: EduRef, id: "edu" },
{ ref: ContactRef, id: "contact" },
];
const visibleSections = sections
.filter(({ ref }) => {
const rect = ref.current.getBoundingClientRect();
return (
rect.top <= window.innerHeight / 2 &&
rect.bottom >= window.innerHeight / 2
);
})
.map(({ id }) => id);
setVisibleSections(visibleSections);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<div className="App">
<Header
moveToHome={moveToHomeHandler}
moveToArc={moveToArcHandler}
moveToSkill={moveToSkillsHandler}
moveToPrj={moveToProjectHandler}
moveToEdu={moveToEduHandler}
moveToContact={moveToContactHandler}
/>
<Home ref={HomeRef} moveToArc={moveToArcHandler} />
<div
ref={ArchivingRef}
className={`section ${
visibleSections.includes("archiving") && "visible"
}`}
>
<Archiving />
</div>
<div
ref={SkillsRef}
className={`section ${visibleSections.includes("skills") && "visible"}`}
>
<Skills />
</div>
<div
ref={ProjectRef}
className={`section ${
visibleSections.includes("project") && "visible"
}`}
>
<Project />
</div>
<div
ref={EduRef}
className={`section ${visibleSections.includes("edu") && "visible"}`}
>
<Edu />
</div>
<div
ref={ContactRef}
className={`section ${
visibleSections.includes("contact") && "visible"
}`}
>
<Contact />
</div>
</div>
);
}
export default App;
+ 추가한 사소한 수정 사항:
- 상위 이동 버튼 삭제
'웹 프로젝트 > 🔮포트폴리오 사이트' 카테고리의 다른 글
[React] 포트폴리오 웹 사이트 제작 일지-11. 프로젝트 이미지 슬라이더 수정(캐러셀 슬라이드), 프로젝트 완성 (1) | 2024.01.09 |
---|---|
[React] 포트폴리오 웹 사이트 제작 일지-10. 프로젝트 슬라이더 만들기 및 반응형 웹으로 디자인 수정 (1) | 2024.01.06 |
[React] 포트폴리오 웹 사이트 제작 일지-8. 스타일링 수정, 포트폴리오 깃허브 배포 (0) | 2023.11.25 |
[React] 포트폴리오 웹 사이트 제작 일지-7. 전체 스타일링 수정, 디자인 변경 (1) | 2023.10.15 |
[React] 포트폴리오 웹 사이트 제작 일지-6. 최상단 이동버튼, Tech Skill 기술스택 추가, Portfollio 내용 추가 (1) | 2023.10.14 |
[React] 포트폴리오 웹 사이트 제작 일지-5. 상단 메뉴바 구현 (useref 를 이용한 컴포넌트 간 스크롤 이동) (0) | 2023.10.13 |