useRef는 .current 속성을 가진 가변 ref 객체를 생성하는 React 훅으로, 컴포넌트 렌더링 간에 유지됩니다. useState와 달리 .current를 변경해도 리렌더링이 발생하지 않아, useRef는 DOM 참조, 타이머 및 UI 재렌더링 없이 렌더링 간에 유지해야 하는 모든 값을 저장하는 데 이상적입니다. React 문서(2025)에 따르면, ref 객체는 컴포넌트 수명 동안 한 번 생성되며 렌더링 간에 변경되지 않아 참조 안정성을 보장합니다.
핵심 요점
useRef는 React 16.8에 추가된 훅으로, 단일 .current 속성을 가진 가변 ref 객체를 반환합니다. 초기 값은 컴포넌트 마운트 시 .current에 할당됩니다. 일반 변수와의 주요 차이점은 ref 객체가 리렌더링에서 살아남는다는 것입니다. 이후 렌더링에서 React는 ref를 다시 생성하지 않고 동일한 객체를 반환합니다.
내부적으로 useRef는 useState 및 useEffect와 동일한 메커니즘(파이버 노드의 훅 연결 리스트)을 사용합니다. 그러나 useState와 달리 React는 ref 객체의 변경을 추적하지 않으며, 업데이트를 대기열에 넣지 않고, 리렌더링을 예약하지 않습니다. 이는 useRef를 빈번한 변경에서도 성능에 영향을 주지 않는 매우 가벼운 훅으로 만듭니다.
React 팀 — Hooks FAQ(2025)에 따르면, useRef는 본질적으로 값을 저장하는 “상자”입니다. 구문 설탕: const ref = useRef(초기값)은 const ref = { current: 초기값 }과 동일하지만, React가 모든 렌더링에서 동일한 ref 객체를 보장한다는 차이가 있습니다. 마법이 아닌 React가 관리하는 안정적인 참조일 뿐입니다.
import { useRef } from 'react';
function Component() {
const countRef = useRef(0);
// countRef.current = 0 초기 상태
// countRef.current = 5 변경 후
// 리렌더링이 발생하지 않습니다!
}
useRef의 가장 일반적인 사용 사례는 DOM 요소에 직접 액세스하는 것입니다. React는 컴포넌트 마운트 후 elementRef.current에 DOM 노드 참조를 할당합니다. 이는 input 포커싱, 요소 크기 측정, 애니메이션 라이브러리 및 React 렌더링을 사용하지 않는 라이브러리와의 통합에 필요합니다.
React는 DOM에서 ref 참조를 자동으로 관리합니다. 마운트 시 실제 DOM 노드를 할당하고, 언마운트 시 null로 설정합니다. 이는 .current가 항상 최신 값을 유지하고, 제거된 요소에 대한 매달린 참조를 통한 메모리 누수를 방지합니다. 수동 정리가 필요하지 않습니다.
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// 컴포넌트 마운트 후 input 포커스
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
React — Refs and DOM 문서(2025)에 따르면, 선언적 작업(모달 열기/닫기, 가시성 관리)에는 ref를 사용하지 마세요. 이러한 목적에는 state와 props가 있습니다. Ref는 선언적으로 표현할 수 없는 명령형 작업(포커스, 텍스트 선택, 타사 라이브러리 통합)을 위한 것입니다.
두 번째로 중요한 사용 사례는 렌더링 간에 유지되어야 하지만 변경으로 인해 리렌더링이 트리거되어서는 안 되는 임의 값 저장입니다. 일반적인 예: 타이머 식별자(setInterval/setTimeout), 요청 취소 플래그, 비교를 위한 이전 prop 값 및 UI 출력에 영향을 주지 않는 모든 데이터.
이는 useEffect에서 특히 유용합니다. useRef는 타이머 식별자를 저장하고 클린업이 이를 지웁니다. timerId가 useState에 저장된 경우, 각 setTimerId 호출은 로직이나 UI에 필요하지 않은 불필요한 리렌더링을 발생시킵니다. useRef는 오버헤드 없이 불필요한 리렌더링 없이 이 문제를 해결합니다.
function Timer() {
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
console.log('틱');
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
useEffect(() => stop, []);
return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}
주요 딜레마 — useRef와 useState 중 선택 — 은 한 가지 질문으로 해결됩니다: “값이 변경될 때 리렌더링이 필요한가?” 예 — useState. 아니요 — useRef. useState는 컴포넌트 출력에 영향을 주는 상태를 저장합니다. useRef는 내부 로직에 필요하지만 UI에 영향을 주지 않는 데이터를 저장합니다.
실제로 개발자는 클로저 문제를 피하기 위해 콜백을 저장하는 목적으로 useRef를 자주 사용합니다. 예를 들어, useEffect가 이벤트를 구독하고 콜백이 현재 state를 필요로 하는 경우 콜백을 useRef에 저장합니다. 각 렌더링에서 ref.current를 새 함수로 업데이트하면, 효과는 재구독 없이 항상 최신 콜백을 호출합니다.
| 기준 | useRef | useState |
|---|---|---|
| 리렌더링 | 변경 시 트리거 안 함 | 각 setState 시 트리거 |
| 변경 | 직접: ref.current = 값 | 세터 통해: setState(값) |
| JSX 사용 | 사용 안 함 (효과 없음) | 컴포넌트 출력에서 사용 |
| 예시 | 타이머, DOM 참조, 이전 값 | 폼 데이터, UI 상태, 플래그 |
안티 패턴이 있습니다: JSX에 필요한 데이터에 useRef를 사용하면서 변경으로 인해 리렌더링이 발생하지 않아야 하는 경우입니다. 이는 역동기화를 초래합니다 — ref.current는 이미 새로운데 UI는 오래된 데이터를 표시합니다. 값이 UI에 표시되는 경우 useState를 사용하세요. 내부적으로만 사용되는 경우 useRef를 사용하세요.
useRef + useEffect 조합은 이전 prop 값을 추적하는 표준 패턴입니다. 이전 값을 ref에 저장하고, useEffect에서 현재 값과 비교한 후 차이에 기반하여 결정을 내립니다. 이는 변경 전의 값을 알아야 하는 애니메이션에서 특히 유용합니다.
function PriceDisplay({ price }) {
const prevPriceRef = useRef(price);
useEffect(() => {
const prevPrice = prevPriceRef.current;
if (price > prevPrice) {
animateUp();
} else if (price < prevPrice) {
animateDown();
}
prevPriceRef.current = price;
}, [price]);
return <span>${price}</span>;
}
React 문서 — Hooks FAQ(2025)에 따르면, 이 패턴을 “이전 값 패턴”이라고 합니다. useRef가 렌더링 간에 값을 유지하고, useEffect가 변경 사항이 커밋된 후 실행되기 때문에 작동합니다. 먼저 DOM이 새 price로 업데이트된 다음, useEffect가 이를 (아직 ref에 있는) 이전 값과 비교하고 ref.current를 현재 값으로 업데이트합니다.
가장 빈번한 실수는 JSX를 계산하기 위해 렌더링 단계에서 ref.current를 읽는 것입니다. ref.current 변경이 리렌더링을 트리거하지 않으므로 컴포넌트가 오래된 값을 사용할 수 있습니다. .current가 UI 출력에 관여하는 경우 useState를 사용하세요. ref와 state를 동기화해야 하는 경우, useEffect를 사용하여 ref에서 state를 업데이트하세요.
// ❌ 표시를 위해 렌더링 중에 ref.current를 읽지 마세요
function BadComponent() {
const valRef = useRef(0);
return <p>{valRef.current}</p>; // 변경 시 업데이트되지 않음
}
// ✅ 표시에는 state, 로직에는 ref를 사용하세요
function GoodComponent() {
const [val, setVal] = useState(0);
const valRef = useRef(0);
return <p>{val}</p>;
}
또 다른 일반적인 실수는 데이터가 실제로 UI에 영향을 미치는 경우 컴포넌트에서 상태를 저장하는 유일한 방법으로 useRef를 사용하는 것입니다. 개발자는 “불필요한” 리렌더링을 피하기 위해 useRef를 선택하지만, 결국 업데이트되지 않는 UI를 얻게 됩니다. 올바른 접근 방식: UI 데이터에는 useState를 사용하고, 렌더링에 관여하지 않는 보조 값에만 useRef를 사용하세요.
자주 묻는 질문
네, 일반적인 패턴입니다 — 이전 상태 패턴. ref를 만들고 추적하는 값이 변경될 때마다 useEffect에서 업데이트하세요. 렌더링과 효과 사이에 ref.current에는 이전 값이 포함되어 있으며, 현재 값과 비교할 수 있습니다. 추가 리렌더링이 필요하지 않으며 모든 데이터 타입에서 작동합니다.
React는 요소가 렌더링되어 실제 DOM에 추가된 후에만 ref.current에 DOM 노드 값을 할당합니다. 첫 번째 렌더링 중에는 컴포넌트가 아직 마운트되지 않았으므로 ref.current는 초기 값(null)과 같습니다. ref.current를 통한 DOM 액세스는 useEffect 내에서 또는 마운트 후에 호출되는 이벤트 핸들러에서만 가능합니다.
createRef는 렌더링마다 새 ref 객체를 생성합니다 — 클래스 컴포넌트에서만 사용해야 합니다. useRef는 ref를 한 번 생성하고 이후 모든 렌더링에서 동일한 객체를 반환합니다. 함수형 컴포넌트에서는 useRef만 사용하세요. createRef는 객체가 다시 생성되므로 리렌더링 시 값 손실을 초래합니다.
네, ref 객체는 일반 prop으로 전달할 수 있습니다: <Child inputRef={inputRef} />. 자식 컴포넌트는 inputRef.current를 통해 사용합니다. 그러나 ref를 자식 컴포넌트의 DOM 요소에 직접 전달하려면 forwardRef — props를 통해 ref를 전달할 수 있는 HOC를 사용하세요. forwardRef가 없으면 ref는 자식 컴포넌트 내부의 DOM 요소에 자동으로 바인딩되지 않습니다.
요소에 ref를 바인딩한 다음, useEffect에서 ref.current.getBoundingClientRect() 또는 ref.current.offsetWidth / offsetHeight를 읽으세요. 크기 변경의 반응형 추적을 위해 useEffect 내에서 ResizeObserver를 사용하세요: observer를 생성하고, 변경을 구독하고, 새 치수로 state를 업데이트하세요. 클린업에서 observer 연결을 해제하는 것을 잊지 마세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.