useState 대신 useRef로 React 애플리케이션 성능 최적화하기 (문제 해결)

Devinix·2024년 2월 27일
0

[문제 해결]

목록 보기
16/29

개요

스크롤 방향에 반응하여 작동하는 애니메이션을 구현하였다. 구현에 큰 문제는 없었지만, 너무 많은 리렌더링을 유발하는 이슈가 있었다. 이 글에서는 불필요한 리렌더링을 방지하여 성능을 최적화 하는 과정에 대해서 다룰 것이다.

이전 글
https://velog.io/@dpldpl/framer-motion%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B0%90%EC%A7%80-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-React-Typescript

문제 상황

기존의 코드

import { AnimationControls, useAnimation } from "framer-motion";
import { MutableRefObject, useEffect, useRef, useState } from "react";

interface IReturn {
  homeContentsRef: MutableRefObject<HTMLDivElement | null>;
  controls: AnimationControls;
}

function useSlideFilterOption(): IReturn {
  const [scrollY, setScrollY] = useState<number>(0); // 현재 스크롤 위치
  const [lastScrollY, setLastScrollY] = useState<number>(0); // 마지막 스크롤 위치
  const homeContentsRef = useRef<HTMLDivElement | null>(null); // 스크롤 영역 컨텐츠
  const [scrollDirection, setScrollDirection] = useState<"up" | "down" | null>(
    null
  ); // 스크롤 방향을 저장하기 위한 상태 변수
  
  // Framer Motion 애니메이션 컨트롤러
  const controls = useAnimation();

  // 스크롤 이벤트를 처리하기 위한 useEffect
  useEffect(() => {
    const handleScroll = () => {
      if (homeContentsRef.current) {
        const homeContentsScrollY = homeContentsRef.current.scrollTop;
        setScrollY(homeContentsScrollY);

        // 스크롤 방향 판단 로직
        if (scrollY > lastScrollY && scrollY >= 60) {
          setScrollDirection("down");
        } else if (scrollY < lastScrollY && scrollY >= 60) {
          setScrollDirection("up");
        } else {
          setScrollDirection(null);
        }

        setLastScrollY(scrollY);
      }
    };

    const element = homeContentsRef.current;
    element?.addEventListener("scroll", handleScroll);

    // 컴포넌트 언마운트 시 이벤트 리스너 제거
    return () => {
      element?.removeEventListener("scroll", handleScroll);
    };
  }, [scrollY, lastScrollY]); // 의존성 배열에 scrollY, lastScrollY 포함

  // 스크롤 방향에 따라 애니메이션 실행
  useEffect(() => {
    if (scrollDirection === "up") {
      controls.start({
        y: 0,
        opacity: 1,
        transition: { type: "tween", duration: 0.3 },
      });
    } else if (scrollDirection === "down") {
      controls.start({
        y: -60,
        opacity: 0,
        transition: { type: "tween", duration: 0.3 },
      });
    }
  }, [scrollDirection, controls]); // 의존성 배열에 scrollDirection, controls 포함

  return { homeContentsRef, controls };
}

export default useSlideFilterOption;

useState를 사용하여 스크롤 위치를 저장하고, 스크롤 방향에 따라 상태를 업데이트하여 애니메이션을 제어하는 과정이다. 그러나 이 방식은 스크롤 이벤트가 발생할 때마다 상태 업데이트를 통해 컴포넌트를 리렌더링하게 만들었고, 결과적으로 불필요한 성능 저하를 초래했다.

해결 과정

useState에서 useRef로 변경

스크롤 위치와 마지막 스크롤 위치를 저장하기 위해 useState 대신 useRef를 사용하였다. useRef는 컴포넌트 리렌더링 없이 값의 변경을 가능하게 하여 성능을 향상시킬 수 있다.

useEffect 의존성 배열 최적화

스크롤 이벤트 리스너를 추가하고 제거하는 useEffect의 의존성 배열을 빈 배열로 설정하여, 컴포넌트 마운트와 언마운트 시에만 실행되도록 최적화하였다. 이는 불필요한 이벤트 리스너 추가/제거 호출을 방지한다.

바뀐 코드

import { AnimationControls, useAnimation } from "framer-motion";
import { MutableRefObject, useEffect, useRef, useState } from "react";

interface IReturn {
  homeContentsRef: MutableRefObject<HTMLDivElement | null>;
  controls: AnimationControls;
}

function useSlideFilterOption(): IReturn {
  const homeContentsRef = useRef<HTMLDivElement | null>(null);
  const controls = useAnimation();
  const scrollYRef = useRef<number>(0); // useRef 사용
  const lastScrollYRef = useRef<number>(0); // useRef 사용  
  const [scrollDirection, setScrollDirection] = useState<"up" | "down" | null>(
    null
  );

  // 컴포넌트가 마운트될 때와 스크롤 이벤트가 발생할 때 실행
  useEffect(() => {
    const handleScroll = () => {
      if (homeContentsRef.current) {
        // 현재 스크롤 위치를 계산
        const homeContentsScrollY = homeContentsRef.current.scrollTop;
        scrollYRef.current = homeContentsScrollY;

        // 스크롤 방향에 따라 상태 업데이트
        if (scrollYRef.current >= 60) {
          if (scrollYRef.current > lastScrollYRef.current) {
            setScrollDirection("down");
          } else if (scrollYRef.current < lastScrollYRef.current) {
            setScrollDirection("up");
          } else {
            setScrollDirection(null);
          }
        }

        // 마지막 스크롤 위치를 업데이트
        lastScrollYRef.current = scrollYRef.current;
      }
    };

    const homeContentsEl = homeContentsRef.current;
    homeContentsEl?.addEventListener("scroll", handleScroll);

    return () => {
      homeContentsEl?.removeEventListener("scroll", handleScroll);
    };
  }, []); // 의존성 배열 제거

  // 스크롤 방향이 변경될 때 애니메이션을 적용
  useEffect(() => {
    if (scrollDirection === "up") {
      controls.start({
        y: 0,
        opacity: 1,
        transition: { type: "tween", duration: 0.3 },
      });
    } else if (scrollDirection === "down") {
      controls.start({
        y: -60,
        opacity: 0,
        transition: { type: "tween", duration: 0.3 },
      });
    }
  }, [scrollDirection, controls]);

  return { homeContentsRef, controls };
}

export default useSlideFilterOption;

결론

기존 코드에서 스크롤을 하단까지 내렸을 때

최적화된 코드에서 스크롤을 하단까지 내렸을 때

이 이슈를 통해 React에서 렌더링 성능을 향상시키는 핵심 전략을 배웠다. 특히, useRef를 활용하여 스크롤 이벤트가 발생할 때 불필요한 리렌더링을 줄이는 방법을 확인할 수 있었다. 이러한 접근법은 리액트 애플리케이션의 사용자 경험을 높이고, 성능을 최적화하고자 하는 개발자들에게 실질적인 가이드라인을 제공할 것이라 생각한다.

profile
프론트엔드 개발

0개의 댓글