React에서 useRef 이해하기 (혼란 없이)
Source: Dev.to

React를 배우고 있다면 useState와 useEffect를 이미 익혔을 가능성이 높습니다. 그런데 useRef를 만나면 갑자기 상황이… 조금 이상하게 느껴질 수 있습니다.
화면을 업데이트하지도 않고, 리렌더도 일으키지 않습니다. 마치 “React 규칙”을 어기는 것처럼 느껴지죠. 비밀은 useRef가 컴포넌트의 개인적인 영구 저장소라는 점입니다.
그럼 useRef가 실제로 무엇을 하는지, 왜 유용한지, 그리고 머리 아프지 않게 어떻게 사용하는지 살펴보겠습니다.
useRef가 실제로 하는 일
핵심적으로 useRef는 다음과 같은 값을 저장할 수 있는 장소를 제공합니다:
- 렌더 사이에 동일하게 유지됨
- 언제든지 변경 가능
- 변경될 때 재렌더링을 일으키지 않음
const myRef = useRef(initialValue);
React는 다음과 같은 객체를 제공합니다:
{ current: initialValue }
중요한 부분은 .current입니다. 값은 여기서 살아 있습니다. React는 이 객체를 한 번만 생성하고 매 렌더마다 같은 객체를 반환합니다.
useRef가 존재하는 이유
당신은 이렇게 궁금할 수도 있습니다: “왜 모든 것에 useState만 쓰지 않나요?” 차이는 가시성에 있습니다.
- State는 사용자가 볼 수 있는 것(UI)을 위한 것입니다. 상태가 변하면 React가 화면을 다시 그립니다.
- Refs는 백그라운드에서 기억하고 싶은 것들을 위한 것입니다. ref가 변해도 React는 조용히 유지되며 UI에 아무 변화도 주지 않습니다.
일반적인 ref 사용 사례:
- DOM 요소
- 타이머 ID
- WebSocket 연결
- 이전 값
- 플래그 또는 카운터
이를 state에 넣으면 불필요한 재렌더링이 발생합니다. 일반 변수를 사용하면 매 렌더링마다 초기화됩니다. useRef는 렌더링을 트리거하지 않고 메모리를 제공함으로써 이 문제를 정확히 해결합니다.
가장 일반적인 사용법: DOM 접근
This is where most people meet useRef first.
import { useRef } from "react";
function MyComponent() {
const inputRef = useRef(null);
return (
<>
inputRef.current.focus()}>
Focus
);
}
React는 실제 DOM 입력 요소를 inputRef.current에 넣어, focus()와 같은 메서드를 직접 호출할 수 있게 합니다. 이것이 올바른 사용 방법입니다:
- 입력에 포커스 주기
- 요소를 화면에 스크롤하기
- 너비 또는 높이 측정하기
- 비디오 또는 오디오 요소 제어하기
DOM 노드는 state에 넣어서는 안 되며, refs가 올바른 도구입니다.
렌더 사이에 값 유지하기 (재렌더링 없이)
때때로 무언가를 기억해야 하지만 React가 관여하길 원하지 않을 때가 있습니다. 예시: 컴포넌트가 렌더링된 횟수를 세기.
const renderCount = useRef(0);
renderCount.current += 1;
이 값은:
- 매 렌더마다 증가합니다
- 절대 재렌더링을 일으키지 않습니다
- 절대 초기화되지 않습니다
카운터, 플래그, 캐시된 데이터, 혹은 내부 회계 등에 유용합니다.
이전 값 추적하기
React는 기본적으로 “이전 상태”를 제공하지 않습니다. useRef가 그 빈틈을 메워줍니다.
function MyComponent({ value }) {
const prevValue = useRef(value);
useEffect(() => {
prevValue.current = value;
}, [value]);
// now prevValue.current holds the previous `value`
}
value는 현재 값이고, prevValue.current는 이전 값입니다. 이 패턴은 깔끔하고 예측 가능하며 널리 사용됩니다.
외부 객체 저장
React와 전혀 관련 없는 것들이 있습니다:
- WebSocket 연결
AbortController인스턴스- 옵저버
- 서드파티 라이브러리 객체
function useWebSocket(url) {
const socketRef = useRef(null);
useEffect(() => {
socketRef.current = new WebSocket(url);
return () => socketRef.current.close();
}, [url]);
return socketRef.current;
}
이러한 객체들은 안정적인 저장 위치가 필요합니다; useRef가 이를 제공합니다. 상태에 넣는 것은 설계상의 실수입니다.
useRef가 아닌 것
useState를 대체하는 것이 아닙니다.- 반응형이 아니며 UI를 업데이트하는 데 사용해서는 안 됩니다.
UI가 값에 의존한다면 state를 사용하세요. React가 해당 값을 알 필요가 없다면 ref를 사용하세요. 이 규칙을 따르면 대부분의 실수를 방지할 수 있습니다.
간단한 정신 모델
useRef를 컴포넌트에 붙어 있는 작은 상자라고 생각해 보세요:
- React가 컴포넌트를 렌더링합니다.
- 상자는 렌더링 사이에 동일하게 유지됩니다.
- 상자 안에 무엇이든 넣을 수 있습니다 (
.current). - React는 그 상자를 완전히 무시합니다.
그게 전부입니다.
최종 생각
useRef는 복잡하지 않으며—그냥 솔직합니다. React가 관리해서는 안 되는 것들을 저장할 수 있는 장소를 제공합니다. 이를 이해하면 “고급”이라는 느낌이 사라지고 당연하게 느껴집니다. 완전히 이해했을 때, 컴포넌트는 다음과 같이 됩니다:
- 더 깔끔해짐
- 더 예측 가능함
- 이해하기 쉬워짐
그것은 마법이 아니라—올바른 작업에 올바른 도구를 사용하는 것입니다.