
useState와 useEffect는 렌더링 흐름을 중심으로 상태와 사이드 이펙트를 관리한다. 하지만 렌더링과 무관하게 값을 기억하거나, DOM을 직접 조작해야 하는 순간도 있다. 이럴 때 React는 useRef라는 훅을 제공한다.
이번 글에서는 useRef의 기본 개념부터, 실무에서 활용할 수 있는 심화 패턴까지 정리해보았다.
useRef는 다음 두 가지 역할을 수행하는 Hook이다.
const ref = useRef(initialValue);
ref.current에 저장한 값은 컴포넌트가 리렌더링돼도 유지된다.ref.current를 변경해도 컴포넌트는 리렌더링되지 않는다.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 | useState |
|---|---|---|
| 값 변경 시 렌더링 | 아니오 | 예 |
| 값 유지 | 렌더링 사이에 유지됨 | 유지됨 |
| 주요 용도 | 값 추적, 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="입력 후 잠시 멈춰보세요" />
);
}
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>
);
}
useRef()는 렌더링과 무관한 값을 저장할 수 있다..current 값을 변경해도 리렌더링은 발생하지 않는다.useState, 내부에서만 추적할 값은 useRef로 구분해서 사용한다.useRef는 React 함수형 컴포넌트에서 렌더링 없이 값을 보존하거나, DOM 요소를 직접 조작해야 할 때 유용한 도구다.
UI 상태는 useState, 내부 로직이나 참조 추적은 useRef로 구분하면 컴포넌트를 더 명확하고 효율적으로 관리할 수 있다.