ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] useState와 useRef를 통한 상태 관리 (애니메이션 상태 최적화)
    React 2024. 12. 30. 18:55

    리액트에서 훅은 함수형 컴포넌트에서 상태와 생명주기 기능을 사용할 수 있게 해주는 강력한 도구입니다.

    그중에서도 useState와 useRef는 상태 관리를 위해 자주 사용됩니다.

     

    하지만 코드를 보다 보면 어떤 상황에서는 useState로,

    또 어떤 경우에는 useRef로 상태를 관리하는 모습을 종종 보게 됩니다.

     

    특히 DOM 조작을 위해 useRef를 사용하는 경우는 자주 보며 이해하기 쉽지만,

    이 외에 이유로 상태 관리를 위해 useRef를 사용하는 사례는 헷갈릴 때가 있습니다.

     

    이번 글에서 이 두 가지 훅을 통한 상태 관리의 차이점을 공부해 보고,

    상황에 따라 어떤 것을 사용하는 것이 적절한지 정리해 보겠습니다.

    | React의 상태 관리와 UI 업데이트 방식

    리액트는 상태(state)나 props의 변경에 따라 컴포넌트를 재렌더링하여 UI를 업데이트합니다.

    이 과정은 리액트의 렌더링 사이클을 통해 이루어지며,

    상태 관리를 위해 주로 useState와 useRef 같은 훅이 사용됩니다.

     

    이때 상태 관리와 UI 업데이트 방식은 크게 두 가지로 나눌 수 있을 거 같습니다.

    1. 리액트의 렌더링 사이클을 통한 업데이트
      - 상태나 props가 변경되면 리액트는 해당 컴포넌트를 재렌더링하고, 이를 통해 UI가 자동으로 갱신됩니다.
    2. 직접적인 DOM 조작을 통한 업데이트
      - 리액트를 거치지 않고, DOM 요소의 속성을 직접 변경하여 UI를 업데이트합니다.

    이 두 방식은 각각 장단접이 있으며, 리액트에서는 상황에 따라 적합한 상태 관리 방식을 선택해야 합니다.

    | useState와 useRef

    useState

    useState는 상태 값이 변경될 때 컴포넌트를 재렌더링하며, 보통 UI와 상태를 동기화하는 데 사용됩니다.

    이를 통해 UI는 최신 상태를 반영하게 됩니다.

     

    useState 상태의 초기값을 설정할 수 있고, 초기값은 반드시 렌더링 시접에 결정됩니다.

    또한, 상태 값 변경이 비동기적으로 처리되고,

    여러 상태 업데이트가 있을 경우 리액트는 이를 효율적으로 배치(batch)하여 한번에 처리합니다.

     

    보통 UI와 동기화 되는 데이터 관리를 위해 사용되고, 흔한 예시로는 카운터 기능이 있습니다.

    import { useState } from "react";
    
    const Counter = () => {
      const [count, setCount] = useState<number>(0);
    
      const handleIncrement = () => setCount((prev) => prev + 1);
    
      return (
        <div>
          <p>현재 카운트: {count}</p>
          <button onClick={handleIncrement}>증가</button>
        </div>
      );
    };
    
    export default Counter;

    여기서 count는 useState를 통해 관리되는 상태 값입니다.

    상태가 변경될 때 컴포넌트가 재렌더링되어, 새로운 값이 UI에 반영됩니다.

    useRef

    useRef는 값이 변경되어도 컴포넌트를 재렌더링하지 않으며,

    컴포넌트의 수명 주기 동안 동일한 값을 유지합니다.

     

    보통 useRef를 사용하여 DOM 요소를 직접 참조하거나 제어하고,

    값은 ref.current를 통해 동기적으로 업데이트됩니다.

     

    또한 상태 변경이 자주 발생하더라도 성능에 영향을 주지 않기 때문에

    애니메이션 작업과 같은 빠르게 변하는 값을 관리하거나 이전 값을 추적하는 데 사용되기도 합니다.

     

    다음은 useRef를 활용하여 버튼을 클릭해 입력 필드를 포커싱 하는 코드입니다.

    import { useRef } from "react";
    
    const App = () => {
      const inputRef = useRef<HTMLInputElement>(null);
    
      const handleFocus = () => {
        if (inputRef.current) {
          inputRef.current.focus();
        }
      };
    
      return (
        <div>
          <input ref={inputRef} type="text" placeholder="입력하세요" />
          <button onClick={handleFocus}>포커스 이동</button>
        </div>
      );
    };
    
    export default App;

    위 코드에서 inputRef는 useRef를 사용하여 DOM 요소를 참조합니다.

    버튼을 클릭하면 DOM 조작을 통해 입력 필드에 포커스가 설정됩니다.

    이 작업은 상태 변경이 필요 없으므로 useRef를 사용하는 것이 적합합니다.

    | 애니메이션 작업에서의 useRef

    애니메이션 작업에서는 상태를 지속적으로 업데이트해야 하지만,

    상태 변경으로 인해 렌더링 비용이 높아지는 것을 방지해야 합니다.

    이런 경우 useRef는 컴포넌트를 재렌더링하지 않고 값을 업데이트할 수 있습니다.

     

    예시로 progress 값을 사용해 애니메이션의 진행 상태를 추적하는 코드를

    간단하게 작성해 보겠습니다.

    import { useRef, useEffect } from "react";
    
    const App = () => {
      const requestRef = useRef<number>();
      const progress = useRef<number>(0);
    
      const animate = () => {
        progress.current += 1;
        console.log(`진행 상태: ${progress.current}`);
    
        requestRef.current = requestAnimationFrame(animate);
      };
    
      useEffect(() => {
        requestRef.current = requestAnimationFrame(animate);
        return () => {
          if (requestRef.current) {
            cancelAnimationFrame(requestRef.current);
          }
        };
      }, []);
    
      return <div>애니메이션 실행 중...</div>;
    };
    
    export default App;

    위 코드에서 progress는 애니메이션의 진행 상태를 저장하는 변수입니다.

    애니메이션의 progress 상태는 화면에 직접적으로 반영되지 않습니다.

    즉, 이 값이 변경되어도 컴포넌트를 다시 렌더링 할 필요가 없으므로 useRef를 사용하여 성능을 최적화합니다.

     

    만약 위 코드에서 useRef가 아닌 useState를 사용해 progress 값을 관리하면 어떻게 될까요?

    그렇게 되면 매 프레임마다 상태를 업데이트하며 컴포넌트가 재렌더링되어 성능 저하가 생길 겁니다.

    한번 위에 예시 코드를 useState로 작성해 보겠습니다.

    import { useState, useEffect } from "react";
    
    const App = () => {
      const [progress, setProgress] = useState<number>(0);
    
      const animate = () => {
        setProgress((prevProgress) => prevProgress + 1); // 상태 업데이트
    
        requestAnimationFrame(animate);
      };
    
      useEffect(() => {
        const animationId = requestAnimationFrame(animate);
        return () => cancelAnimationFrame(animationId);
      }, []);
    
      useEffect(() => {
        console.log(`진행 상태: ${progress}`);
      }, [progress]);
    
      return <div>애니메이션 실행 중...</div>;
    };
    
    export default App;

    이런 식으로 코드를 작성하면 setProgress가 호출되면

    리액트에서 상태가 변경되었다고 판단하고 컴포넌트를 재렌더링합니다.

    이는 애니메이션이 끊기거나 느려지는 원인이 될 수 있습니다.

     

    리액트 프로파일러의 컴포넌트 렌더링 하이라이팅을 보면 아래와 같이

    useState를 사용했을 때 컴포넌트가 계속 렌더링 되는 것을 확인해 볼 수 있습니다.

     

    useState 사용

    반면 useRef를 사용했을 때는 렌더링이 발생하지 않는 것을 볼 수 있습니다.

    useRef 사용

    이런 식으로 애니메이션 상태를 지속적으로 업데이트하면서도 렌더링을 방지할 수 있어

    성능 최적화에 적합합니다.

    | UI 변경에 대한 의문점과 해소

    이 주제에 대해 관심을 갖게 된 계기는,

    다른 사람이 구현한 슬라이드 기능 코드를 보고 의문을 가지게 되면서였습니다.

     

    저는 주로 useRef를 DOM 요소를 참조하거나 조작하는 경우에만 사용했고,

    대부분의 상태는 useState로 관리했습니다.

     

    하지만 제가 본 다른 사람의 코드에서는 UI 변경에 직접적으로 영향을 미치지 않는 값들을

    모두 useRef로 관리하고 있었습니다.

    예를 들면

    const isDragging = useRef<boolean>(false); // 사용자의 드래그 여부
    const startX = useRef<number>(0); // 드래그 시작 시 마우스 좌표
    const scrollLeft = useRef<number>(0); // 슬라이드 초기 스크롤 위치
    const animationProgress = useRef<number>(0); // 애니메이션 진행 상태

     

     

     

    이처럼 렌더링이 필요 없는 값들은 모두 useRef로 관리되고 있었습니다.

     

    이를 통해 상태 변경이 UI에 직접적으로 영향을 미치지 않는 경우,

    굳이 useState를 사용할 필요가 없다는 것을 배웠습니다.

    | 끝

    리액트로 프로젝트를 개발할 때, UI 변경의 방식을 먼저 고려해야 합니다.

    1. 리액트의 렌더링 사이클을 통한 UI 변경인지
      - 상태 변경이 UI와 직접적으로 연동된다면 useState를 사용합니다
    2. 직접적인 DOM 조작을 통한 UI 변경인지
      - 상태 변경이 렌더링을 트리거할 필요가 없다면 useRef로 관리하여 성능을 최적화할 수 있습니다.

    이처럼 상태 변경과 UI 변경의 연관성을 잘 판단하여,

    적절한 훅을 선택하는 것이 리액트 개발의 중요한 부분임을 다시금 느끼게 되었습니다.


    참고

    https://react.dev/reference/react/useRef

     

    useRef – React

    The library for web and native user interfaces

    react.dev

    https://react.dev/reference/react/useState

     

    useState – React

    The library for web and native user interfaces

    react.dev

     

Designed by Tistory.