React는 state와 props를 통해 UI를 선언적으로 표현하는 것을 권장합니다. 하지만 포커스 설정, 스크롤 제어, 외부 라이브러리 연동처럼 DOM에 직접 접근해야 하는 경우가 있습니다. useRef는 이런 상황에서 React의 렌더링 흐름 밖에서 값을 유지하거나 DOM 요소에 직접 접근할 수 있는 수단을 제공합니다.
useRef는 .current 프로퍼티를 가진 객체를 반환합니다. 초기값을 인자로 전달하면 .current의 초기값이 됩니다.
const ref = useRef(initialValue);
// ref = { current: initialValue }
이 객체는 컴포넌트가 살아있는 동안 동일한 객체가 유지됩니다. .current 값을 변경해도 리렌더링이 발생하지 않으며, 리렌더링이 일어나도 값이 초기화되지 않습니다.
JSX 요소의 ref prop에 ref 객체를 전달하면, 해당 DOM 요소가 마운트된 후 .current에 DOM 노드가 자동으로 연결됩니다.
function SearchInput() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus(); // DOM API 직접 호출
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>포커스</button>
</>
);
}
컴포넌트가 언마운트되면 .current는 자동으로 null로 초기화됩니다. DOM 접근은 마운트 이후(useEffect 내부나 이벤트 핸들러)에서 해야 합니다.
useRef는 DOM 참조 외에도 렌더링과 무관하게 값을 보관하는 용도로 사용합니다. 예를 들어 이전 렌더링의 state 값을 기억하거나, 타이머 ID를 저장하거나, 외부 라이브러리 인스턴스를 유지할 때 유용합니다.
function StopWatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setTime((t) => t + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<>
<p>{time}초</p>
<button onClick={start}>시작</button>
<button onClick={stop}>정지</button>
</>
);
}
타이머 ID를 state에 저장하면 변경 시 불필요한 리렌더링이 발생합니다. useRef에 저장하면 리렌더링 없이 값을 유지하면서 언제든지 읽고 쓸 수 있습니다.
ref는 일반 prop과 달리 자식 컴포넌트로 자동 전달되지 않습니다. 부모가 자식 컴포넌트 내부의 DOM 요소에 ref를 연결하려면 forwardRef를 사용해야 합니다.
const FancyInput = forwardRef((props, ref) => {
return <input ref={ref} className="fancy" {...props} />;
});
function Parent() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<>
<FancyInput ref={inputRef} />
<button onClick={handleClick}>포커스</button>
</>
);
}
forwardRef로 감싼 컴포넌트는 두 번째 인자로 부모가 전달한 ref를 받아 내부 DOM 요소에 연결합니다. 디자인 시스템이나 공용 컴포넌트 라이브러리를 만들 때 자주 사용하는 패턴입니다.
Ref는 강력한 탈출구이지만, React의 선언적 패러다임을 우회하는 수단이므로 DOM 접근이 꼭 필요한 경우에만 사용하는 것이 좋습니다. state로 해결할 수 있는 문제에 ref를 쓰면 코드 예측이 어려워집니다.