무엇
useRef는 렌더 사이에 유지되는 값을 담는 상자를 만드는 훅이다. 값은 current 프로퍼티 하나에 들어 있고, 이 값을 바꿔도 컴포넌트는 다시 그려지지 않는다.
initialValue는 current의 첫 값으로 쓰이고, 이후 렌더에서는 무시된다. 돌려주는 것은 { current: ... } 모양의 객체 하나다.
const intervalRef = useRef(0)
const inputRef = useRef<HTMLInputElement>(null)
쓰임은 크게 둘이다. 화면에 안 보이는 값을 기억해 두는 것(타이머 ID 같은), 그리고 DOM 노드를 붙잡는 것이다.
무슨 일이 일어나나
useRef는 매 렌더마다 같은 객체를 돌려준다. 일반 변수는 렌더마다 초기화되지만, ref는 React가 컴포넌트 바깥에 붙들어 두기 때문에 값이 이어진다. 이 점은 useState와 같다.
useState와 갈리는 지점은 하나다. ref.current를 바꿔도 React는 알아채지 못하고, 리렌더도 일으키지 않는다. ref는 그냥 평범한 자바스크립트 객체라 React가 그 변경을 감시하지 않기 때문이다. state는 설정 함수로 바꾸며 매번 리렌더를 예약하지만, ref는 ref.current = x로 직접 대입하고 화면은 그대로 둔다. 그래서 화면에 보여야 하는 값이면 state, 렌더와 무관하게 기억만 하면 되는 메타데이터면 ref다.
ref 어트리뷰트로 JSX 엘리먼트에 넘기면, React가 그 DOM 노드가 붙는 시점에 ref.current를 노드로 채우고 떨어질 때 null로 되돌린다.
사용법
렌더와 상관없는 값을 기억할 때 쓴다. 이벤트 핸들러에서만 읽고 쓰는 값, 예컨대 인터벌 ID처럼 화면에 나타나지 않는 것이다.
import { useRef, useState } from 'react'
export function Stopwatch() {
const [now, setNow] = useState(0)
const intervalRef = useRef<number | null>(null) // 리렌더와 무관한 값
const start = () => {
intervalRef.current = window.setInterval(() => setNow((n) => n + 1), 1000)
}
const stop = () => {
if (intervalRef.current !== null) clearInterval(intervalRef.current)
}
return (
<>
<span>{now}s</span>
<button onClick={start}>시작</button>
<button onClick={stop}>정지</button>
</>
)
}
DOM 노드를 다룰 때 쓴다. ref를 엘리먼트에 걸고, 이벤트 핸들러에서 ref.current로 노드의 메서드를 부른다.
function Form() {
const inputRef = useRef<HTMLInputElement>(null)
const focus = () => inputRef.current?.focus()
return (
<>
<input ref={inputRef} />
<button onClick={focus}>입력란에 포커스</button>
</>
)
}
만들기 비싼 값은 한 번만 만든다. useRef(new VideoPlayer())처럼 쓰면 매 렌더마다 인스턴스가 생기고 버려진다. null로 시작해 필요할 때 한 번만 채운다.
const playerRef = useRef<VideoPlayer | null>(null)
if (playerRef.current === null) {
playerRef.current = new VideoPlayer() // 첫 렌더에서 딱 한 번
}
실무 예시
이전 값과 현재 값을 나란히 보여주는 경우다. 방금 전 값은 화면 갱신을 유발할 필요가 없으니 ref에 담아 두고, 렌더가 끝난 뒤 갱신한다.
import { useEffect, useRef } from 'react'
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined)
useEffect(() => {
ref.current = value // 렌더 이후에만 쓴다
}, [value])
return ref.current // 이번 렌더에는 직전 값이 담겨 있다
}
왜 중요한가
useRef는 state로는 안 되는 두 가지를 메운다. 리렌더 없이 값을 기억하기, 그리고 DOM에 직접 손대기다. 타이머·이전 값·서드파티 인스턴스 보관, 포커스·스크롤·측정 같은 명령형 처리가 전부 여기서 나온다. state만큼 자주 쓰지는 않고 대개 특정 상황에서 꺼내 쓰는 도구지만, 이게 없으면 우회하기 어려운 자리가 분명하다.
Reference