본문 바로가기

STUDY/React

[React 포트폴리오 페이지 제작기] 2. 풀페이지 스크롤링 기능 구현 (FEAT : useRef & useEffect & useState & useCallback)

728x90

GitHub Pages 배포 이후 첫 작업은 페이지를 섹션으로 나눠서

한 화면에서 스크롤링을 이용하여 섹션 별로 이동하게 하는 기능이다.

이를 풀페이지 스크롤링이라고 한다.

라이브러리를 사용하는 것과 직접 CSS와 JavaScript를 사용하여 구현하는 방법이 있는데,

나의 성장을 위해 진행하는 프로젝트이기 때문에 후자를 선택하였다.

 

✨ Styled-components

CSS는 styled-components를 사용하였다.

 

import styled from "styled-components";

const Container = styled.div`
  height: 100vh;	//페이지 높이를 전체 화면으로 설정
  overflow: hidden;	//스크롤 숨김
`;

const Section = styled.section`
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: start; //스크롤 스냅 기능
`;

 

🖱️ 스크롤 스냅 기능

  • 스크롤링해서 화면을 위, 아래로 이동할 때, 현재 화면과 다음 화면 사이의 절대 비율이 50%가 넘어갈 경우, 비율이 높은 쪽으로 자석처럼 달라붙는 효과를 구현해주는 기능

 

//섹션 나누기
function App() {
    return (
        <Container ref={containerRef}>
          <Section>
            <Main />
          </Section>
          <Section>
            <Introduce />
          </Section>
          <Section>
            <Career />
          </Section>
          <Section>
            <Project />
          </Section>
          <Section>
            <Footer />
          </Section>
        </Container>
      );
    }
}

 

먼저 각 페이지 별로 섹션을 나눠주었다.

여기서 Container에 있는 ref={containerRef} 를 볼 수 있다.

이것에 대해 알기 위해서 먼저 useRef에 대해 알아야한다.

코드를 보기 전에 먼저 사용할 React Hook들에 대해서 알아보았다.

 


📖 useState

state를 사용하기 위한 Hook

  • 사용
const [변수명, set함수명] = useState(초기값);

 

📖 useRef

React Hook의 한 종류로 특정한 DOM요소에 접근이 가능하여 불필요한 재렌더링 X

 

형식

  • 생성
const containerRef = useRef(null);
  • 결과 값으로 {current : 초기값} 을 지닌 객체 반환 (current라는 키값을 지닌 프로퍼티가 생성, 값에 변경을 줄 때 current 이용)
  • 반환 요소에 접근
<Container ref={containerRef}>
  • 특징
    • 반환된 useRef 객체는 컴포넌트의 전생애주기를 통해 유지
    • 컴포넌트가 계속해서 렌더링 되어도 컴포넌트가 언마운트되기 전까지 값 유지
    • current 속성은 값을 변경해도 상태 변경처럼 컴포넌트 재렌더링 X (렌더링과 상과없이, 마운트된 시점 → 언마운트된 시점까지 값 유지)
    • 렌더링 이후에도 값은 유지, 변수는 초기화
  • 사용
    • 값을 저장하는 저장공간
      • 변경 시 렌더링을 발생시키지 않아야 할 값을 다룰 때 사용(변화만 감지)
    • DOM요소에 접근
      • 예시 : input 요소를 클릭하지 않아도 focus를 줄 때
  • 장점
    • state에 값을 담으면, 변경될 때마다 재렌더링 → 성능 저하
    • useRef 사용 시 값이 변경되도 렌더링 X → .성능 향상

 

📖 useEffect

React의 함수 컴포넌트에서 Side effect(부작용)를 실행할 수 있게 하기 위한 Hook

다른 커모넌트에 영향을 미칠 수 있으며, 렌더링 중에는 작업이 완료될수 없기 때문

 

  • 사용
useEffect(() => {
  // 매 렌더링마다 실행
});

useEffect(() => {
  // 컴포넌트가 처음 렌더링된 실행
}, []);

useEffect(() => {
  // 컴포넌트가 처음 렌더링된 이후 실행
  // a나 b가 변경되어 컴포넌트가 재렌더링된 이후 실행
}, [a, b]);

 

📖 useCallback

 

값이 아닌 함수를 반환

  • 사용
const memoizedCallback = useCallback(
	() => {
		doSomething(의존성 변수1, 의존성 변수2);
	},
	[의존성 변수1, 의존성 변수2]
);

 

이제 구현한 코드를 살펴보도록 하자

 

handleScroll 함수

사용자가 마우슬 휠을 움직일 때마다 호출되는 이벤트 헨들러

useCallback 훅을 사용해 메모이제이션 되어있음

이 함수가 종속된 값들이 변경되지 않는 한, 컴포넌트가 다시 렌더링될 때마다 새로운 함수로 재생성되지 않음

  • 함수 정의
// useCallback으로 (isThrottled, sectionCount)이 변경되지 않는 한 동일한 함수 인스턴스 유지
const handleScroll = useCallback(
  (event) => {
    if (isThrottled) return; // 스크롤 이벤트가 너무 자주 발생하지않게 방지
    // 스크롤 이벤트를 처리하는 동안 isThrottled 상태를 true로 설정, 다음 스크롤 이벤트 처리 방지
    setIsThrottled(true); 
    // 스크롤 이벤트가 너무 자주 발생하지 않도록 스로틀링(throttling) 기법을 사용합니다.
  • 스크롤 방향에 따른 섹션 변경
    const deltaY = event.deltaY;
    // event.deltaY: 마우스 휠 스크롤의 수직 이동량을 나타냅니다.
	
    if (deltaY > 0) {
      //스크롤이 아래로 이동한 경우	
      setCurrentSection((prev) => Math.min(prev + 1, sectionCount - 1));
      //이전 섹션 인덱스에서 +1, 최대 섹션 수를 초과하지 않도록 제한
    } else {
      //스크롤이 위로 이동한 경우
      setCurrentSection((prev) => Math.max(prev - 1, 0));
      //이전 섹션 인덱스에서 -1, 최솟값인 0을 벗어나지 않도록 제한
    }
    // 스크롤 방향에 따라 현재 섹션을 업데이트합니다.
  • 스크롤링 설정
   //800ms 후에 isThrottled 상태를 다시 false로 설정하여 다음 스크롤 이벤트 처리할 수 있게 함
   setTimeout(() => setIsThrottled(false), 800);
  },
  [isThrottled, sectionCount] // 의존성 배열 설정으로 값이 변경될 때마다 함수 다시 생성
);

 

useEffect 훅을 활용한 이벤트 리스너 관리 및 스크롤 제어

React에서 useEffect를 사용하여 컴포넌트가 마운트될 때 특정 이번트 리스터를 추가하고,

언마운트될 때 이를 제거하는 방법으로, 상태 변화에 따라 동적으로 스크롤 위치를 변경하는 기능 구현

 

 

첫 번째 useEffect

  • 이벤트 리스너 관리
    • 컴포넌트가 마운트될 때 wheel 이벤트 리스너를 추가하고, 언마운트될 때 제거하는 코드
const containerRef = useRef(null); 
// useRef 훅을 사용해 참조 객체 생성, 이 객체는 DOM 요소 참조

useEffect(() => {
  const container = containerRef.current;
  // containerRef의 현재 값(즉, DOM 요소)을 가져옴

  container.addEventListener("wheel", handleScroll);
  // wheel 이벤트가 발생할 때 호출할 이벤트 핸들러(handleScroll) 추가

  return () => {
    container.removeEventListener("wheel", handleScroll);
    // 컴포넌트가 언마운트되거나 의존성(handleScroll)이 변경될 때 이벤트 리스너 제거
  };
}, [handleScroll]); 
// 의존성 배열에 handleScroll을 추가하여 handleScroll이 변경될 때마다 이펙트가 다시 실행

 

두 번째 useEffect

  • 상태 변화에 따른 스크롤 제어
    • currentSection 상태가 변경될 때마다 컨테이너를 해당 섹션 위치로 부드럽게 스크롤
useEffect(() => {
  const container = containerRef.current;
  // containerRef의 현재 값(DOM 요소)을 가져옴

  container.scrollTo({
    top: currentSection * window.innerHeight,
    // currentSection 값에 따라 스크롤할 Y 좌표 계산
    behavior: "smooth", 
    // 스크롤이 부드럽게 이동하도록 애니메이션 설정
  });
}, [currentSection]); 
// currentSection이 변경될 때마다 이펙트 다시 실행

 

 

 

✨ 느낀 점

라이브러리 없이 직접 구현하는 것에 대한 어려움에 대해서 깨달았다. 해보지 않은 것을 맨땅에서 시작하는 것이 좀 쉽지않았다. 하지만 검색해보고 몰랐던 것에 대해 알게 되고 코드를 따라쳐보고 내가 원하는대로 수정하며 구현하다 보니 코드에 대해 이해하게 되었다.

처음에는 받아쓰기를 하는 느낌이었는데 그 받아쓴 코드를 이해하고 내가 원하는 대로 수정하면서 나의 것이 되었다.

어떠한 기능을 구현하고자 하면 어떠한 React Hook이 사용되어야할지 잘 생각하고 코드를 짜야할 것 같다.