useRef는 React가 컴포넌트가 살아 있는 동안 유지하는 객체 { current: initialValue }를 반환합니다. ref.current를 바꿔도 아무것도 다시 렌더링되지 않습니다. 그래서 ref는 두 가지 일에 적합합니다. focus() 같은 메서드를 호출할 수 있도록 DOM 노드를 보관하는 일, 그리고 화면에 보여 줄 필요가 없는 값을 렌더링 사이에 기억하는 일입니다.
ref 객체를 ref={inputRef}에 넘기면 React가 <input> DOM 노드를 inputRef.current에 넣습니다. focus()를 select()로 바꾸고, 무언가를 입력한 다음 다시 클릭해 보세요. 텍스트가 선택됩니다.
문법
const myRef = useRef(initialValue);
myRef.current; // read
myRef.current = value; // write, no re-render
initialValue는 첫 렌더링에서만 사용됩니다. DOM ref에서는null입니다.- React는 렌더링마다 같은 객체를 반환합니다. 바뀌는 것은
current뿐입니다. current를 읽고 쓰는 것은 일반 자바스크립트입니다. React는 이를 추적하지 않습니다.
렌더링을 일으키지 않는 값
state는 화면이 보여 주는 것을 위한 것입니다. ref는 컴포넌트가 기억해야 하지만 표시하지는 않는 것을 위한 것입니다. 이 블록은 카운터 하나는 ref에, 하나는 state에 두고 둘 다 로그로 남깁니다.
Ref + 1을 몇 번 클릭해 보세요. 콘솔에서는 ref.current가 올라가지만, 페이지는 여전히 0이고 렌더링 로그도 없습니다. 이제 State + 1을 클릭하세요. 컴포넌트가 렌더링되고, 화면의 ref 카운터가 ref가 이미 가지고 있던 숫자로 뛰어오릅니다. ref는 내내 값을 유지하고 있었고, React에게는 그것을 그릴 이유가 없었을 뿐입니다.
ref와 state
useState | useRef | |
|---|---|---|
| 반환값 | [value, setValue] | { current: value } |
| 바꾸면 | 컴포넌트가 다시 렌더링됨 | 눈에 보이는 변화 없음 |
| 새 값을 읽을 수 있는 시점 | 다음 렌더링에서 | 즉시 |
| 바꾸는 방법 | setter | 할당, ref.current = x |
| 용도 | 화면에 보이는 모든 것 | 타이머 id, DOM 노드, 이전 값, 플래그 |
확신이 서지 않으면 JSX가 그 값을 읽는지 물어보세요. 읽는다면 state에 속합니다.
타이머 id 저장하기
setInterval은 나중에 타이머를 멈출 때 필요한 id를 반환합니다. ref에 잘 맞는 값입니다. Stop 핸들러에는 필요하지만 화면에는 절대 보이지 않으며, 일반 변수에 두면 다음 렌더링에서 잃어버립니다.
useRef(null)을 let intervalRef = { current: null };로 바꾸고 Start를 누른 뒤 2초 기다렸다가 Stop을 눌러 보세요. 타이머가 계속 갑니다. 틱마다 컴포넌트가 렌더링되어 새 객체가 만들어지므로 Stop은 아무것도 정리하지 못합니다.
이전 값 기억하기
이펙트는 렌더링 후에 실행되므로, 새 값을 ref에 저장된 값과 비교한 다음 다음번을 위해 새 값을 저장할 수 있습니다.
green을 클릭한 다음 blue를 클릭하세요. 콘솔에 changed from red to green과 changed from green to blue가 기록됩니다. 이미 선택된 색을 클릭하면 이펙트가 color가 바뀔 때만 실행되므로 아무것도 기록되지 않습니다.
DOM 다루기
ref는 React에 prop이 없는 브라우저 API를 호출하는 방법입니다. focus(), select(), scrollIntoView(), 비디오의 play(), 또는 getBoundingClientRect()로 크기 읽기 같은 것들입니다.
const lastItemRef = useRef(null);
function scrollToLast() {
lastItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
<li ref={lastItemRef}>Last item</li>
ref.current는 React가 아직 DOM 노드를 만들지 않았으므로 첫 렌더링 중에는 null이고, 요소가 제거된 뒤에도 다시 null입니다. 노드가 존재하는 이벤트 핸들러와 이펙트에서 읽으세요. 요소를 측정하고 브라우저가 그리기 전에 무언가를 배치하려면 useLayoutEffect에서 ref를 읽으세요.
렌더링 중에는 ref.current를 읽거나 쓰지 마세요
React는 컴포넌트 본문이 순수한 계산이기를 기대합니다. 같은 props와 state면 같은 JSX입니다. ref는 그 모델 바깥에서 바뀌므로, ref를 읽거나 쓰는 렌더링은 같은 입력에 대해 다른 결과를 보여 줄 수 있고, React는 본문을 예상보다 자주 호출할 수도 있습니다.
function Bad() {
const count = useRef(0);
count.current++; // writing during render
return <p>{count.current}</p>; // reading during render
}
ref는 이벤트 핸들러와 이펙트에서 읽고 쓰세요. 유일한 예외는 지연 초기화로, 첫 렌더링에서 ref를 한 번 채우는 경우입니다.
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer(); // runs once
}
(위의 ref 카운터 데모는 화면이 ref를 따라가지 않는다는 것을 보여 주기 위해 일부러 JSX에서 clicks.current를 읽습니다. 실제 코드에 그대로 옮기지 마세요.)
ref 콜백
ref 객체 대신 ref에 함수를 넘길 수 있습니다. React는 요소가 추가될 때 DOM 노드와 함께 그 함수를 호출합니다. React 19에서는 함수가 클린업을 반환할 수 있으며, React는 요소가 제거될 때 그 클린업을 호출합니다. 항목마다 useRef를 한 번씩 호출할 수 없는 목록에서 편리합니다.
로드되면 콘솔에 항목마다 attached 줄이 하나씩 기록됩니다. Remove last를 클릭하면 detached Cheese만 나타납니다. trackNode는 컴포넌트 바깥에 정의되어 있어 렌더링마다 같은 함수이므로 React는 다른 항목들을 그대로 둡니다. 대신 ref={(node) => { ... }}처럼 인라인으로 쓰면, 렌더링마다 새 함수가 넘어가므로 클릭할 때마다 모든 항목이 분리되었다가 다시 연결됩니다.
직접 만든 컴포넌트에 ref 넘기기
React 19에서 ref는 함수 컴포넌트의 일반 prop입니다. 부모가 ref를 만들고, 자식은 노출하고 싶은 DOM 요소에 그것을 붙입니다.
React 19 이전에는 forwardRef가 필요했으며, 지금도 동작합니다. forwardRef 페이지에서 두 버전과 useImperativeHandle로 메서드 몇 개만 노출하는 방법을 보여 줍니다.
자주 하는 실수
ref 값을 JSX에 보여 주기. 화면은 ref를 따라가지 않습니다. 사용자가 값을 봐야 한다면 state를 사용하세요.
ref.current를 이펙트 의존성으로 쓰기. [ref.current]는 렌더링 중에 값을 읽는데 React는 그 값이 언제 바뀌는지 모르므로, 값이 바뀌어도 이펙트가 다시 실행되지 않습니다. 노드가 연결되는 것에 반응하려면 ref 콜백을 사용하세요.
첫 렌더링 중에 DOM ref 읽기. React가 노드를 만들기 전까지는 null입니다. 이펙트나 핸들러에서 읽으세요.
훅을 쓰지 않고 새 객체 만들기. 컴포넌트 본문의 const ref = { current: null }은 렌더링마다 새 객체입니다. 매번 같은 객체를 주는 것은 useRef뿐입니다.
자주 묻는 질문
useRef는 무엇을 반환하나요?
속성이 current 하나뿐인 일반 객체이며, 넘긴 초기값으로 설정됩니다. React는 렌더링마다 같은 객체를 반환하므로 current에 넣은 것은 다음에도 그대로 있습니다.
useRef와 useState의 차이는 무엇인가요?
state를 바꾸면 컴포넌트가 다시 렌더링되지만, ref.current를 바꾸면 렌더링되지 않습니다. 화면에 보이는 값에는 state를, 타이머 id나 DOM 노드처럼 이벤트 핸들러와 이펙트만 필요로 하는 값에는 ref를 사용하세요.
ref.current가 null인 이유는 무엇인가요?
React는 DOM 노드를 만든 뒤 커밋 단계에서 ref.current를 그 노드로 설정합니다. 첫 렌더링 중에는 아직 null이고, 요소가 제거된 뒤에도 다시 null이 됩니다. 이벤트 핸들러나 이펙트에서 읽으세요.
ref.current를 바꾸면 리렌더링이 일어나나요?
아니요. React는 값이 바뀐 것을 모릅니다. state 업데이트처럼 다른 무언가가 컴포넌트를 다시 렌더링할 때만 새 값이 화면에 나타납니다.
자식 컴포넌트에 ref를 넘기려면 어떻게 하나요?
React 19에서 함수 컴포넌트는 ref를 일반 prop으로 받으므로 <MyInput ref={inputRef} />로 넘기면 자식이 그것을 자기 <input>에 붙입니다. 이전 버전에서는 forwardRef가 필요했습니다.