[React Hook 완전 정복] Step 3: useRef 기초와 활용

Suyo·2025년 6월 17일


useStateuseEffect는 렌더링 흐름을 중심으로 상태와 사이드 이펙트를 관리한다. 하지만 렌더링과 무관하게 값을 기억하거나, DOM을 직접 조작해야 하는 순간도 있다. 이럴 때 React는 useRef라는 훅을 제공한다.

이번 글에서는 useRef의 기본 개념부터, 실무에서 활용할 수 있는 심화 패턴까지 정리해보았다.


useRef란?

useRef는 다음 두 가지 역할을 수행하는 Hook이다.

  1. 렌더링과 상관없이 값을 저장하는 변수처럼 사용
  2. DOM 요소를 직접 참조하기 위한 수단
const ref = useRef(initialValue);
  • ref.current에 저장한 값은 컴포넌트가 리렌더링돼도 유지된다.
  • ref.current를 변경해도 컴포넌트는 리렌더링되지 않는다.

기본 사용법

DOM 참조 예제: input 포커스 제어

import { useRef } from 'react';

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

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} placeholder="클릭 시 포커스됨" />
      <button onClick={handleClick}>포커스</button>
    </div>
  );
}

값 추적 예제: 렌더링 횟수 카운팅

import { useRef, useState } from 'react';

function RenderCounter() {
  const renderCount = useRef(1);
  const [value, setValue] = useState("");

  renderCount.current += 1;

  return (
    <div>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <p>렌더링 횟수: {renderCount.current}</p>
    </div>
  );
}

이전 값 기억하기

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

function PreviousValueTracker() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(count);

  useEffect(() => {
    prevCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>현재 값: {count}</p>
      <p>이전 값: {prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

useRef vs useState 차이

항목useRefuseState
값 변경 시 렌더링아니오
값 유지렌더링 사이에 유지됨유지됨
주요 용도값 추적, DOM 접근UI 상태 관리
불변성 관리필요 없음필요함

실무 활용 예제 (심화)

스크롤 위치 추적

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

function ScrollTracker() {
  const scrollY = useRef(0);
  const [message, setMessage] = useState("");

  useEffect(() => {
    const handleScroll = () => {
      scrollY.current = window.scrollY;
      if (scrollY.current > 300) {
        setMessage("스크롤이 300px 이상입니다!");
      } else {
        setMessage("");
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

  return <p>{message}</p>;
}

디바운싱 타이머 제어

import { useRef, useState } from 'react';

function DebouncedInput() {
  const [text, setText] = useState("");
  const timerRef = useRef(null);

  const handleChange = (e) => {
    const value = e.target.value;
    clearTimeout(timerRef.current);
    timerRef.current = setTimeout(() => {
      console.log("서버 전송: ", value);
    }, 500);
    setText(value);
  };

  return (
    <input value={text} onChange={handleChange} placeholder="입력 후 잠시 멈춰보세요" />
  );
}

IntersectionObserver 활용

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

function LazyBox() {
  const boxRef = useRef(null);
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setVisible(entry.isIntersecting);
    });

    if (boxRef.current) {
      observer.observe(boxRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div
      ref={boxRef}
      style={{
        marginTop: "500px",
        height: "200px",
        background: visible ? "skyblue" : "gray",
        transition: "background 0.3s",
      }}
    >
      {visible ? "보이고 있어요!" : "아직 안 보여요"}
    </div>
  );
}

핵심 정리

  1. useRef()는 렌더링과 무관한 값을 저장할 수 있다.
  2. .current 값을 변경해도 리렌더링은 발생하지 않는다.
  3. DOM 요소에 접근하거나 이전 값을 추적하는 데 매우 유용하다.
  4. 디바운싱, IntersectionObserver, 외부 API 타이머 등을 다룰 때 필수 도구다.
  5. 화면에 표시할 값은 useState, 내부에서만 추적할 값은 useRef로 구분해서 사용한다.

마무리

useRef는 React 함수형 컴포넌트에서 렌더링 없이 값을 보존하거나, DOM 요소를 직접 조작해야 할 때 유용한 도구다.
UI 상태는 useState, 내부 로직이나 참조 추적은 useRef로 구분하면 컴포넌트를 더 명확하고 효율적으로 관리할 수 있다.


참고 자료

profile
Mee-

0개의 댓글