1. 기본 문법
useRef는 단 하나의 속성 current를 가진다.
이 속성은 언제든 읽고 쓸 수 있지만, useState와 다르게 값이 바뀐다고 해서 컴포넌트가 다시 렌더링되지는 않는다.
import { useRef } from 'react';
// 초기값과 함께 생성
const myRef = useRef(initialValue);
// 값 읽기
const value = myRef.current;
// 값 변경
myRef.current = newValue;
이처럼 current에 직접 값을 넣거나 꺼내 쓰는 방식이다.
2. useRef의 유일한 속성: current
useRef가 가진 건 단 하나, 바로 current 속성이다.
const ref = useRef(0);
// 읽기
console.log(ref.current); // 0
// 쓰기
ref.current = 10;
console.log(ref.current); // 10
// 객체도 가능
const objRef = useRef({ count: 0, name: 'test' });
objRef.current.count = 5;
여기서 중요한 점은 값을 바꿔도 리렌더링이 발생하지 않는다는 것이다.
그래서 UI에 보여줘야 하는 값은 useState를 쓰고, 컴포넌트 안에서만 관리할 값이라면 useRef를 쓰는 게 적절하다.
3. DOM 요소 참조
useRef가 가장 많이 쓰이는 용도는 DOM 요소를 직접 참조하는 것이다. 예를 들어 비디오를 재생하거나, input에 포커스를 줄 수 있다.
import { useRef, useEffect } from 'react';
function VideoPlayer() {
const videoRef = useRef(null);
const inputRef = useRef(null);
useEffect(() => {
// DOM 메서드 직접 호출
videoRef.current.play();
videoRef.current.volume = 0.5;
// input에 포커스
inputRef.current.focus();
}, []);
return (
<>
<video ref={videoRef} />
<input ref={inputRef} />
</>
);
}
DOM 요소에서는 다양한 메서드를 쓸 수 있다.
// Video 요소
videoRef.current.play();
videoRef.current.pause();
videoRef.current.currentTime = 10;
videoRef.current.volume = 0.5;
// Input 요소
inputRef.current.focus();
inputRef.current.blur();
inputRef.current.select();
inputRef.current.value = 'text';
// 모든 요소
elementRef.current.scrollIntoView();
elementRef.current.getBoundingClientRect();
elementRef.current.classList.add('active');
4. 이전 값 저장
렌더링이 일어날 때마다 이전 값을 저장하고 싶을 때 useRef가 유용하다.
예를 들어 카운터 값이 변경될 때 이전 값도 함께 보여줄 수 있다.
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<div>
<p>현재: {count}</p>
<p>이전: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
5. 타이머와 인터벌 관리
setInterval이나 setTimeout 같은 비동기 작업을 관리할 때도 useRef를 쓸 수 있다.
특히 컴포넌트가 언마운트될 때 정리하는 코드를 넣으면 메모리 누수를 막을 수 있다.
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const startTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
useEffect(() => {
return () => {
clearInterval(intervalRef.current);
};
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={startTimer}>시작</button>
<button onClick={stopTimer}>정지</button>
</div>
);
}
6. 렌더링 횟수 추적
useRef는 렌더링 횟수를 기록하는 데에도 자주 쓰인다. 값이 바뀌어도 화면은 다시 그려지지 않기 때문이다.
function RenderCounter() {
const renderCount = useRef(0);
renderCount.current += 1;
return <div>렌더링 횟수: {renderCount.current}</div>;
}
7. 디바운싱과 쓰로틀링
검색창 같은 곳에서 입력이 빠르게 들어올 때 API 호출을 최소화하려면 디바운싱을 적용해야 한다. 이때 useRef로 타이머를 관리하면 된다.
function SearchInput() {
const [query, setQuery] = useState('');
const debounceRef = useRef(null);
const handleSearch = (value) => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
console.log('검색:', value);
}, 500);
};
return (
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
handleSearch(e.target.value);
}}
/>
);
}
8. 이벤트 상태 플래그
특정 이벤트를 한 번만 실행하거나, 일정 간격으로 실행되도록 하고 싶을 때도 useRef가 편하다.
function VideoPlayer() {
const hasCompletedRef = useRef(false);
const lastSaveTimeRef = useRef(0);
const handleComplete = () => {
if (!hasCompletedRef.current) {
hasCompletedRef.current = true;
console.log('완료 처리');
}
};
const handleSave = () => {
const now = Date.now();
if (now - lastSaveTimeRef.current > 5000) {
lastSaveTimeRef.current = now;
console.log('저장');
}
};
return <div>Video Player</div>;
}
9. useRef와 useState의 차이
많은 주니어 개발자가 헷갈려 하는 부분이다.
언제 useRef를 쓰고, 언제 useState를 써야 하는지 정리하면 아래와 같다.
// useRef
const ref = useRef(value);
ref.current = newValue; // 값 변경 → 리렌더링 없음
// useState
const [state, setState] = useState(value);
setState(newValue); // 값 변경 → 리렌더링 발생
- UI에 보여야 하는 값 → useState
- 내부적으로만 관리하는 값 → useRef
10. 정리와 주의사항
마지막으로, useRef는 다음 상황에서 꼭 정리(clean-up)를 해주는 것이 좋다.
useEffect(() => {
return () => {
clearTimeout(timerRef.current);
};
}, []);
그리고 중요한 점 하나.
useRef 값은 바뀌어도 컴포넌트는 다시 렌더링되지 않는다.
따라서 화면에 보여줘야 하는 값이라면 반드시 useState를 써야 한다.