프로그래밍/ReactJS

React 개발의 숨은 보석: `useRef` 훅 완전 정복!

shimdh 2025. 10. 15. 10:30
728x90

React로 애플리케이션을 개발하다 보면, 상태(state)나 속성(props)만으로는 해결하기 어려운 순간이 종종 찾아옵니다. 불필요한 리렌더링 없이 특정 DOM 요소에 접근해야 하거나, 컴포넌트의 수명 주기 동안 변경 가능한 값을 유지해야 할 때 말입니다. 바로 이런 상황에서 useRef 훅이 빛을 발합니다. 이 훅은 React의 강력한 도구로, DOM 조작부터 성능 최적화까지 다양한 영역에서 개발자들의 구원투수가 되어줍니다. 오늘은 useRef의 기본 개념부터 실전 활용 사례까지 깊이 파헤쳐보겠습니다. 초보자부터 고급 개발자까지 유용한 팁을 가득 담았으니, 끝까지 함께 따라와 주세요!

useRef란 무엇인가요?

useRef 훅은 컴포넌트의 전체 수명 주기 동안 유지되는 변경 가능한 참조(reference) 를 생성합니다. 가장 중요한 점은 ref에 저장된 값이 변경되어도 리렌더링을 유발하지 않는다는 것입니다. 이는 useState처럼 상태 변경 시 컴포넌트가 다시 렌더링되는 것과 완전히 다릅니다.

useRef는 주로 두 가지 목적으로 사용됩니다:

  • DOM 요소나 React 요소에 접근: .current 속성을 통해 직접 조작할 수 있습니다.
  • 변경 가능한 값 저장: UI 업데이트와 무관한 데이터(예: 타이머 ID, 이전 값)를 저장합니다.

이 특성 덕분에 useRef는 불필요한 리렌더링을 피하면서도 유연한 코드를 작성할 수 있게 해줍니다. 이제 실제 활용 사례를 통해 그 매력을 느껴보죠!

728x90

useRef의 주요 활용 사례

useRef는 단순한 DOM 접근을 넘어 다양한 시나리오에서 빛을 발합니다. 아래에서 구체적인 예시와 함께 살펴보겠습니다.

1. DOM 요소에 직접 접근하고 조작하기

useRef의 대표적인 용도는 DOM 노드에 직접 접근하는 것입니다. 예를 들어, 컴포넌트가 마운트될 때 입력 필드에 자동으로 포커스를 맞추는 경우를 생각해보세요.

import React, { useEffect, useRef } from 'react';

const FocusInput = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    // 컴포넌트 마운트 후 입력 요소에 포커스 맞추기
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} type="text" placeholder="여기에 입력하세요" />;
};

이 코드에서 inputRef.current.focus()를 호출하면 사용자가 페이지 로드 직후 입력 필드에 바로 집중할 수 있습니다. 이는 로그인 폼이나 검색 바에서 사용자 경험(UX)을 크게 향상시킵니다.

2. 불필요한 리렌더링 없이 변경 가능한 값 저장하기

타이머 ID나 애니메이션 핸들처럼 UI와 무관하지만 유지해야 할 값을 저장할 때 useRef가 이상적입니다. 아래 예시는 간단한 타이머 컴포넌트입니다.

import React, { useEffect, useState, useRef } from 'react';

const TimerComponent = () => {
  const [count, setCount] = useState(0);
  const timerId = useRef(null); // 타이머 ID를 저장할 ref

  const startTimer = () => {
    if (timerId.current) return; // 중복 실행 방지
    timerId.current = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
  };

  const stopTimer = () => {
    if (timerId.current) {
      clearInterval(timerId.current);
      timerId.current = null;
    }
  };

  // 컴포넌트 언마운트 시 타이머 정리 (메모리 누수 방지)
  useEffect(() => {
    return () => {
      if (timerId.current) {
        clearInterval(timerId.current);
      }
    };
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
};

timerId는 리렌더링 없이 타이머를 제어하며, 컴포넌트가 사라질 때 정리(cleanup)됩니다. 이는 WebSocket 연결이나 이벤트 리스너 같은 경우에도 유용합니다.

3. 렌더링 간에 값 유지하기 (이전 상태 추적)

props나 state의 이전 값을 추적해야 할 때 useRef를 사용하면 리렌더링 없이 값을 유지할 수 있습니다. 아래 예시는 props 변경을 감지하는 컴포넌트입니다.

import React, { useState, useEffect, useRef } from 'react';

const PreviousValueExample = ({ value }) => {
  const prevValueRef = useRef(); // 이전 값을 저장할 ref
  const [isChanged, setIsChanged] = useState(false);

  useEffect(() => {
    // 이전 값으로 현재 값을 저장 (다음 렌더링 전에)
    if (prevValueRef.current !== value) {
      setIsChanged(true);
    }
    prevValueRef.current = value;
  }, [value]);

  return (
    <div>
      <h1>Current Value: {value}</h1>
      <h2>Previous Value: {prevValueRef.current ?? 'None'}</h2>
      {isChanged && <p style={{ color: 'red' }}>값이 변경되었습니다!</p>}
    </div>
  );
};

이 코드는 value props가 변경될 때마다 이전 값을 유지하며, 변경을 감지해 UI를 업데이트합니다. 디버깅이나 애니메이션 트리거에 자주 사용됩니다.

4. 함수형 컴포넌트의 생명주기 관리

함수형 컴포넌트는 클래스 컴포넌트의 this처럼 인스턴스 변수를 가지지 않지만, useRef로 이를 대체할 수 있습니다. 아래 예시는 컴포넌트 마운트 시 한 번만 실행되는 초기화 로직입니다.

import React, { useEffect, useRef } from 'react';

const LifecycleExample = () => {
  const isMounted = useRef(false); // 마운트 여부를 추적할 ref

  useEffect(() => {
    if (!isMounted.current) {
      // 컴포넌트가 처음 마운트될 때만 실행
      console.log('컴포넌트가 마운트되었습니다!');
      isMounted.current = true;
    }

    // 언마운트 시 정리
    return () => {
      isMounted.current = false;
      console.log('컴포넌트가 언마운트되었습니다.');
    };
  }, []);

  return <div>라이프사이클 관리 예시</div>;
};

이처럼 useRefuseEffect와 함께 클래스 컴포넌트의 생명주기 메서드를 흉내낼 수 있어, 외부 라이브러리 통합에 편리합니다.

5. 성능 최적화에 기여

useState는 상태 변경 시 전체 컴포넌트를 리렌더링하지만, useRef는 그렇지 않습니다. 이는 복잡한 계산 결과를 캐싱하거나, 빈번한 업데이트를 피할 때 유용합니다.

예를 들어, 무거운 계산을 한 번만 수행하고 결과를 저장하는 경우:

import React, { useRef, useState } from 'react';

const ExpensiveCalculation = () => {
  const [input, setInput] = useState(0);
  const cacheRef = useRef(new Map()); // 계산 결과를 캐싱할 ref

  const computeExpensiveValue = (num) => {
    if (cacheRef.current.has(num)) {
      return cacheRef.current.get(num);
    }
    // 무거운 계산 시뮬레이션
    const result = num * 1000000; // 실제로는 복잡한 로직
    cacheRef.current.set(num, result);
    return result;
  };

  const handleChange = (e) => {
    setInput(Number(e.target.value));
  };

  return (
    <div>
      <input type="number" value={input} onChange={handleChange} />
      <p>결과: {computeExpensiveValue(input)}</p>
    </div>
  );
};

이 코드는 입력 변경 시 리렌더링 없이 이전 계산 결과를 재사용해 성능을 최적화합니다. 대규모 앱에서 필수적인 패턴입니다.

6. 비제어 컴포넌트 경고 방지 및 폼 제어

React는 제어 컴포넌트를 권장하지만, 비제어 컴포넌트(직접 DOM 관리)도 필요할 때가 있습니다. useRef로 입력 값을 직접 읽고 쓸 수 있어 경고를 피합니다.

import React, { useRef } from 'react';

const UncontrolledForm = () => {
  const inputRef = useRef(null);
  const fileRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append('text', inputRef.current.value);
    formData.append('file', fileRef.current.files[0]);

    // 폼 데이터 처리 (예: API 전송)
    console.log('폼 제출:', Object.fromEntries(formData));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} type="text" placeholder="텍스트 입력" />
      <input ref={fileRef} type="file" />
      <button type="submit">제출</button>
    </form>
  );
};

이 접근은 서드파티 폼 라이브러리나 레거시 코드와의 통합에 적합하며, React의 경고를 피하면서 유연성을 제공합니다.

결론: useRef는 단순한 참조 이상의 가치

useRef 훅은 React 개발의 숨은 보석으로, DOM 조작부터 성능 최적화까지 다채로운 역할을 합니다. 요약하자면:

  • DOM 요소의 직접 조작을 용이하게 합니다.
  • 리렌더링 없이 변경 가능한 값을 저장합니다.
  • 이전 상태 추적과 생명주기 관리를 돕습니다.
  • 비제어 컴포넌트와 성능 향상을 위한 도구로 활용됩니다.
728x90