본문 바로가기
웹 프로젝트/🔮포트폴리오 사이트

[React] 포트폴리오 웹 사이트 제작 일지-9. 스크롤 애니메이션 효과 추가

by 청량리 물냉면 2024. 1. 2.
반응형

현재 포트폴리오에도 필요한 내용은 전부 들어가 있지만, 어딘지 밋밋해 보여서 스크롤을 내리면 컴포넌트 내용이 나타나는 애니메이션을 추가하기로 했다. 

전체 컴포넌트를 관리하고 있는 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에 대한 설명참조

 

[javascript] 윈도우 창 크기 (window.outerWidth, window.outerHeight, window.outerHeight, window.innerWidth, innerHeight)

자바스크립트에서 윈도우 창 크기에 대해 알아보겠습니다. 익스플로러, 크롬, 파이어폭스, 오페라 모두 같은 값을 출력하는 윈도우 창크기는 window.outerWidth, window.outerHeight, window.outerHeight, window.in

sometimes-n.tistory.com

 

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;

스크롤 애니메이션 적용 화면

 

 

 

+ 추가한 사소한 수정 사항:

  • 상위 이동 버튼 삭제
반응형