[React] useRef Hook

soleil_lucy·2025년 11월 9일

useRef 훅을 이해하고 정리해두고 싶어 이 글을 작성하게 됐습니다. 최근 이정환님의 리액트 강의를 들으며 다시 공부하던 중 useRef가 등장했습니다. 그동안은 단순히 코드 예제를 따라 쓰기만 했지, 왜 그렇게 써야하는지? 동작하는지는 잘 몰랐습니다. 이번에 강의 내용과 리액트 공식 문서를 함께 참고하면서 개념을 정리해보려고 합니다.

useRef란?

useRef는 렌더링에 필요하지 않는 값을 참조하거나 저장할 수 있는 React Hook입니다. 리렌더링 사이에서도 값이 유지되지만, 값이 바뀌더라도 컴포넌트를 다시 렌더링하지 않습니다.

const ref = useRef(initialValue);

매개변수

  • initialValue: ref 객체의 current 프로퍼티 초기 설정값입니다. 여기에는 어떤 유형의 값이든 지정할 수 있습니다. 이 인자는 초기 렌더링 이후부터는 무시됩니다.

반환값

useRef는 단일 프로퍼티를 가진 객체를 반환합니다:

  • current: 처음에는 전달한 initialValue로 설정됩니다. 나중에 다른 값으로 바꿀 수 있습니다. ref 객체를 JSX 노드의 ref 어트리뷰트로 React에 전달하면 React는 current 프로퍼티를 설정합니다.

초기화 예시

아래 코드에서 ref는 { current: 0 } 형태의 일반 JavaScript 객체로 생성됩니다. 이 current 값은 컴포넌트가 리렌더링되어도 유지됩니다.

const ref = useRef(0);
// ref = { current : 0 };

왜 필요할까?

렌더링을 유발하지 않고 값을 기억해야 할 때

값을 저장해야 하지만 그 값이 변경되어도 화면을 다시 그릴 필요가 없을 때, useRef를 사용합니다. state와 달리 ref는 값이 바뀌어도 컴포넌트를 리렌더링하지 않습니다.

언제 사용될까?

1️⃣ DOM 요소에 포커스 주기

버튼 클릭 시 input에 자동으로 포커스를 맞출 때 사용합니다.

코드 실행해보러 가기

function SearchForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>검색창 포커스</button>
    </>
  );
}

2️⃣ 타이머 ID 저장하기

setInterval로 시작한 타이머를 나중에 멈추기 위해 interval ID를 저장합니다.

코드 실행해보러 가기

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
      <button onClick={handleStop}>
        Stop
      </button>
    </>
  );
}

3️⃣ 검색창 디바운스 처리

사용자가 타이핑을 멈춘 후 일정 시간이 지나면 검색 API를 호출합니다. 매 입력마다 API를 호출하면 서버에 부담이 되므로, 타이머 ID를 ref에 저장해서 이전 타이머를 취소하고 새로운 타이머를 설정합니다.

코드 실행해보러 가기

function SearchInput() {
  const [keyword, setKeyword] = useState('');
  const timerRef = useRef(null);

  function handleChange(e) {
    const value = e.target.value;
    setKeyword(value);
    
    // 이전 타이머 취소
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
    
    // 500ms 후 검색 API 호출
    timerRef.current = setTimeout(() => {
      // searchAPI(value);
      console.log(`검색 API 호출! 검색어: ${value}`);
    }, 500);
  }

  return (
    <input 
      value={keyword}
      onChange={handleChange}
      placeholder="검색어를 입력하세요"
    />
  );
}

4️⃣ 폼 제출 중복 방지

사용자가 버튼을 여러 번 클릭해도 한 번만 제출되도록 방지합니다. 렌더링 없이 순수하게 중복 제출만 막고 싶을 때 ref를 사용합니다.

코드 실행해보러 가기

function PaymentForm() {
  const [formData, setFormData] = useState({});
  const isSubmittingRef = useRef(false);

  // 가짜 결제 API (2초 후 성공)
  const submitPayment = () => {
    return new Promise((resolve) => {
      setTimeout(() => {
      return resulve('api 호출 성공~');
      // return reject('api 호출 실패...');
			}, 2000);
    });
  };

  const handleSubmit = async () => {
    if (isSubmittingRef.current) return;
    
    isSubmittingRef.current = true;
    
    try {
      await submitPayment(formData);
      alert('결제 완료!');
    } catch (error) {
      alert('결제 실패');
    } finally {
      isSubmittingRef.current = false;
    }
  };

  return (
    <button onClick={handleSubmit}>
      결제하기
    </button>
  );
}

주의할 점

1️⃣ ref 객체에 저장된 값이 렌더링에 사용된다면 변이하지 마세요

ref.current 프로퍼티는 state와 달리 변이할 수 있습니다. 그러나 렌더링에 사용되는 객체(예: state의 일부)를 포함하는 경우 해당 객체를 변이 해서는 안 됩니다.

2️⃣ ref 변경은 리렌더링을 트리거하지 않습니다

ref.current 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않습니다. ref일반 JavaScript 객체이기 때문에 React는 사용자가 언제 변경 했는지 알지 못합니다.

3️⃣ 렌더링 중에는 ref 객체를 읽거나 쓰지 마세요

초기화를 제외 하고는 렌더링 중에 ref.current를 쓰거나 읽지마세요. 이렇게 하면 컴포넌트의 동작을 예측할 수 없게 됩니다.

4️⃣ Strict Mode에서는 ref 객체가 두 번 생성된 후 하나가 버려집니다

Strict Mode에서 React는 컴포넌트 함수를 두 번 호출하여 의도하지 않은 변경을 찾을 수 있도록 돕습니다. 이는 개발 환경 전용 동작이며 Production 환경에는 영향을 미치지 않습니다. 그래서 ref 객체는 두 번 생성되고 그 중 하나는 버려집니다. 컴포넌트 함수가 순수하다면(그래야만 합니다), 컴포넌트의 로직에 영향을 미치지 않습니다.

컴포넌트 함수가 순수하다?

같은 입력(props)에 대해 항상 같은 결과(JSX)를 반환하고, 외부 변수나 객체를 수정하지 않는 함수를 말합니다.

정리하면서

useRef는 리렌더링을 유발하지 않으면서 값을 참조하거나 저장할 때 사용하는 훅이라는 걸 이해하게 되었습니다. useRef 훅의 가장 큰 특징은 값이 변경되어도 리렌더링을 유발하지 않는다는 점을 확실히 기억할 수 있게 되었습니다.

앞으로 프로젝트를 진행하면서 리렌더링 없이 무언가를 처리해야 하는 상황이 생기면 useRef를 떠올릴 수 있을 것 같습니다.

참고 자료

profile
여행과 책을 좋아하는 개발자입니다.

0개의 댓글