처음 useRef를 접했을 땐, 단순히 DOM 요소에 직접 접근할 때 사용하는 훅이라고 생각했다. ex) input 태그에 ref를 달고 focus()를 주는 식
하지만 React의 렌더링 원리를 조금 더 공부한 후, useRef는 단순한 DOM 접근용이 아닌 "렌더링 없이 값을 기억하는 저장소" 라는 걸 알게 되었다.
이번 글에선 useRef를 제대로 파헤쳐보자.
즉, useRef는 렌더링 없이도 값이 유지되는 저장소이며, DOM에 직접 접근할 수 있는 방법이다.
사실 이렇게 글로만 설명하면 확 와닿지가 않는다. (나 한정,,)
-> useRef를 실제로 사용할 때, 어떻게 사용하는지를 알아보자.
const ref = useRef("hi");
console.log(ref); // { current: "hi" }
ref.current = "hello";
console.log(ref); // { current: "hello" }
{ current: 초기값 }✅ 이런 경우 useRef 사용하기
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log("렌더링 횟수:", renderCount.current);
});
→ 값은 잘 바뀌지만 화면엔 영향 없음!
즉 변경시, 렌더링을 발생시키지 말아야하는 값을 다룰 때 정말 편리하다.
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
→ input에 바로 focus를 줄 수 있다.
value, checked 등에도 접근 가능하다.
아래와 같은 코드가 있다고 치자.
import { useRef, useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
const countRef = useRef(0);
const increaseCountState = () => {
setCount(count + 1);
};
const increaseCountRef = () => {
countRef.current = countRef.current + 1;
};
return (
<div>
<p>{count}</p>
<p>{countRef.current}</p>
<button onClick={increaseCountState}>state</button>
<button onClick={increaseCountRef}>ref</button>
</div>
);
};
export default App;
변화는 추적하되, 리렌더링은 원하지 않을 때 ref가 유용하다.
import { useRef, useState } from "react";
const App = () => {
const [render, setRender] = useState(0);
const countRef = useRef(0);
let countVar = 0;
const increaseCountRef = () => {
countRef.current += 1;
console.log("ref:", countRef.current);
};
const increaseVar = () => {
countVar += 1;
console.log("var:", countVar);
};
return (
<div>
<p>{countRef.current}</p>
<p>{countVar}</p>
<button onClick={() => setRender(render + 1)}>render</button>
<button onClick={increaseCountRef}>ref</button>
<button onClick={increaseVar}>var</button>
</div>
);
};
export default App;
let countVar = 0
→ 일반 변수는 컴포넌트 함수가 다시 실행될 때마다 새로 선언되기 때문에
→ 렌더링이 되면 다시 0으로 초기화됨.
const countRef = useRef(0)
→ ref는 React가 컴포넌트 전체 생명주기 동안 유지하는 객체에 값을 저장하므로
→ 렌더링이 몇 번 일어나든 값이 변하지 않고 유지됨.
ex) 현재 컴포넌트가 몇 번 렌더링됐는지를 콘솔에 출력하고 싶다!
❌ 잘못된 방식 (state 사용)
import { useState, useEffect } from "react";
const App = () => {
const [count, setCount] = useState(1);
const [renderCount, setRenderCount] = useState(1); // 렌더링 횟수를 상태로 관리
const increaseCountState = () => {
setCount(count + 1);
};
useEffect(() => {
setRenderCount(renderCount + 1); // 상태 업데이트 → 리렌더링 → 무한 루프!
console.log(renderCount);
});
return (
<div>
<p>{count}</p>
<button onClick={increaseCountState}>state</button>
</div>
);
};
export default App;
-> 무한 렌더링이 발생하고, 아래와 같은 에러가 뜬다.
Maximum update depth exceeded.
✅ 올바른 방식 (useRef 사용)
import { useEffect, useRef, useState } from "react";
const App = () => {
const [count, setCount] = useState(1);
const renderCount = useRef(1); // 렌더링 횟수를 ref로 관리
const increaseCountState = () => {
setCount(count + 1);
};
useEffect(() => {
renderCount.current += 1;
console.log("렌더링 횟수:", renderCount.current);
});
return (
<div>
<p>{count}</p>
<button onClick={increaseCountState}>state</button>
</div>
);
};
export default App;

→ 이렇게 우리가 접근하고자 하는 요소 태그의 ref 속성으로 우리가 만들어준 ref를 넣어죽만 하면 된다. (정말 쉽게 해당요소에 접근할 수 있다,)
ex) input에 강제로 focus를 주고 싶다면? 렌더링과 동시에?
import { useEffect, useRef } from "react";
const App = () => {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
console.log(inputRef);
if (inputRef.current) inputRef.current.focus();
}, []);
return (
<div>
<input type='text' ref={inputRef} />
</div>
);
};
export default App;
이런식으로 input 요소에 접근이 가능하다.
추가적으로 foucs 뿐만 아니라 value나 여러 input 태그의 속성에도 접근이 가능하다.
const login = () =>{
alert(inputRef.current.value);
}
useRef는값의 변화를 추적해야 하지만, 그 변화가 리렌더링을 일으켜선 안 되는 상황에서 매우 유용하다.
React 렌더링 흐름 바깥에서 "조용히 기억"하는 저장소 역할
도대체 어디까지 뿌시나요...