useRef — 화면을 건드리지 않고 값을 기억하기
State와 달리 바뀌어도 리렌더를 일으키지 않는 useRef를 정리합니다. 일반 변수·전역 let과 무엇이 다른지, 왜 렌더링 중엔 읽고 쓰면 안 되는지, 그리고 DOM에 손대는 대표 쓰임까지 다룹니다.
지난 글까지 다룬 State는 “바뀌면 화면을 다시 그리는” 값이었습니다. 그런데 정반대의 요구가 있을 수 있습니다. 값은 렌더링을 넘어 기억하고 싶지만, 그 값이 바뀌어도 화면은 다시 그리고 싶지 않은 경우죠. 이럴 때 쓰는 것이 useRef입니다.
예를 들어 “지난번 스크롤 위치”, “몇 번째 렌더인지 세는 카운터”, “타이머의 ID” 같은 값들은 기억은 해야 하지만 화면에 직접 나타나진 않습니다. 이런 것을 State로 두면 바뀔 때마다 불필요하게 화면을 다시 그리게 됩니다.
useRef — 렌더링에 영향 없는 기억 상자
import { useRef } from 'react';
const ref = useRef(initialValue);
useRef는 { current: 초기값 } 형태의 객체를 돌려줍니다. 값은 ref.current로 읽고 쓰며, 이 상자는 두 가지 성질을 가집니다.
- 바꿔도 리렌더가 일어나지 않는다 (State와의 결정적 차이)
- 리렌더가 일어나도 값이 유지된다 (일반 지역 변수와의 차이)
State와 나란히 두면 성격이 선명합니다.
| 바꾸면 리렌더? | 리렌더 넘어 유지? | 용도 | |
|---|---|---|---|
| State | ✅ 한다 | ✅ | 화면에 반영돼야 하는 값 |
| useRef | ❌ 안 한다 | ✅ | 기억하되 화면과 무관한 값 |
그냥 변수나 전역 let이면 안 될까
“화면을 안 건드리는 기억 값이면, 컴포넌트 밖에 let으로 하나 두면 되지 않나?” 하는 생각이 듭니다. 실제로 그렇게 해도 ‘리렌더 안 하는 유지되는 값’은 만들 수 있습니다. 하지만 결정적 문제가 있습니다.
모듈 바깥의 let은 그 컴포넌트의 모든 인스턴스가 공유합니다. 같은 컴포넌트를 화면에 세 개 띄우면, 세 개가 하나의 변수를 나눠 쓰게 되어 서로의 값을 덮어써 버립니다. 반면 useRef는 각 컴포넌트 인스턴스마다 자기만의 독립된 상자를 갖습니다. 세 개를 띄우면 상자도 세 개, 서로 간섭하지 않죠.
컴포넌트 안의 지역 변수는 반대 문제입니다. 인스턴스마다 독립적이긴 하지만, 앞서 봤듯 렌더링 때마다 다시 실행돼 매번 초기화됩니다. useRef는 “인스턴스마다 독립적이면서, 렌더링을 넘어 유지되는” 두 성질을 동시에 만족시키는 유일한 자리입니다.
렌더링 중에는 읽지도, 쓰지도 말 것
useRef에는 중요한 규칙이 하나 있습니다. 렌더링 도중에 ref.current를 읽거나 쓰지 마세요.
function MyComponent() {
myRef.current = 123; // 🚩 렌더 중 쓰기 금지
return <h1>{myRef.current}</h1>; // 🚩 렌더 중 읽기 금지
}
5편에서 렌더링은 순수해야 한다고 했습니다. 그런데 ref는 바뀌어도 React에 알리지 않는 값이라, 렌더 도중에 이걸 읽거나 쓰면 “언제 바뀐 건지 알 수 없는 값”에 화면이 의존하게 되어 예측 불가능해집니다. 대신 ref는 이벤트 핸들러나 이펙트 안에서 다뤄야 합니다.
function MyComponent() {
useEffect(() => {
myRef.current = 123; // ✅ 이펙트 안에서는 OK
});
function handleClick() {
doSomething(myRef.current); // ✅ 이벤트 핸들러 안에서는 OK
}
}
렌더링이 끝난 뒤, 특정 사건이 벌어졌을 때 한 번씩 만지는 것 — 그게 ref의 올바른 사용 리듬입니다.
가장 흔한 쓰임 — DOM 요소에 손대기
사실 실무에서 useRef를 가장 많이 만나는 곳은 따로 있습니다. 바로 특정 DOM 요소를 직접 가리켜야 할 때입니다. 입력창에 자동으로 포커스를 주거나, 요소의 크기를 재거나, 스크롤을 옮기는 일들이죠.
function SearchBox() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 마운트되면 입력창에 자동 포커스
}, []);
return <input ref={inputRef} />;
}
JSX 요소에 ref={inputRef}를 달아 두면, 렌더 후 React가 그 실제 DOM 노드를 inputRef.current에 넣어 줍니다. 선언형으로 화면을 그리다가도, 이렇게 명령형으로 요소를 직접 다뤄야 하는 순간을 위한 공식 탈출구가 useRef인 셈입니다.
State냐 Ref냐
마지막으로 판단 기준 하나. 그 값이 화면에 보여야 하면 State, 화면과 무관하게 그저 기억만 하면 되면 Ref입니다. 둘을 혼동해 “화면에 보여야 할 값을 ref에 넣어” 화면이 갱신되지 않거나, 반대로 “화면과 무관한 값을 state에 넣어” 불필요한 리렌더를 유발하는 것이 흔한 실수입니다.
이렇게 컴포넌트 하나가 스스로 다루는 값들(State·Ref)까지 왔습니다. 다음 글에서는 시야를 넓혀, 멀리 떨어진 컴포넌트끼리 값을 나눠 갖는 방법 — Props를 한 단계 넘어서는 Context를 살펴보겠습니다.