useRef — 기억하되 다시 그리지 않는다
Article
8강에서 값을 기억하는 두 가지 방법이 실패했다. 함수 안의 변수는 초기화되고, 함수 밖의 변수는 공유된다.
그래서 useState를 썼다. 그런데 useState는 바뀌면 화면을 다시 그린다.
화면과 무관한 값을 기억해야 한다면 어떻게 할까.
다시 그릴 필요가 없는 값
타이머 ID를 저장한다고 하자.
function Timer() {
const [intervalId, setIntervalId] = useState(null);
function start() {
const id = setInterval(tick, 1000);
setIntervalId(id);
}
function stop() {
clearInterval(intervalId);
}
}
동작은 한다. 그런데 setIntervalId를 부르는 순간 컴포넌트가 다시 그려진다. 화면에 아무 변화도 없는데 헛수고다.
useRef를 쓴다.
import { useRef } from "react";
function Timer() {
const intervalRef = useRef(null);
function start() {
intervalRef.current = setInterval(tick, 1000);
}
function stop() {
clearInterval(intervalRef.current);
}
}
useRef는 { current: 값 } 형태의 상자 하나를 돌려준다. 그 상자는 렌더링이 반복돼도 계속 같은 상자다.
렌더링이 몇 번 일어나도 같은 객체를 돌려준다
- intervalRef
- 0x1a2b
- { current: 42 }
state 와 ref
같은 "기억하기"인데 결정적으로 다르다.
useState
화면에 보이는 값
- 바뀌면 다시 그린다
- 직접 수정 금지 (12강)
setState로만 바꾼다- 그 렌더링의 스냅샷이다
useRef
화면과 무관한 값
- 바뀌어도 아무 일 없다
ref.current = x로 직접 수정- 언제든 읽고 쓴다
- 항상 최신값을 준다
판단 기준은 하나다. 이 값이 바뀌면 화면이 바뀌어야 하는가.
| 값 | 무엇으로 | 왜 |
|---|---|---|
| 입력창의 글자 | state | 화면에 보여야 한다 |
| 타이머 ID | ref | 화면과 무관하다 |
| 이전 렌더의 값 | ref | 비교용일 뿐이다 |
| DOM 요소 | ref | 화면을 그리는 재료가 아니다 |
| 클릭 횟수 (표시함) | state | 보여줘야 한다 |
| 클릭 횟수 (기록만) | ref | 안 보여준다 |
렌더 중에 읽고 쓰지 않는다
13강에서 렌더 함수는 순수해야 한다고 했다. ref.current를 렌더 중에 건드리면 그 규칙이 깨진다.
function Bad() {
const ref = useRef(0);
ref.current++;
return <p>{ref.current}</p>;
}
StrictMode에서 두 번 실행되면 값이 두 번 오른다. 순수하지 않다.
이벤트 핸들러나 useEffect 안에서 읽고 쓴다.
DOM 을 직접 다룬다
useRef의 두 번째 쓰임이자 더 흔한 쓰임이다.
React는 DOM을 대신 관리해준다. 하지만 직접 만져야 할 때가 있다.
function SearchInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
ref 속성에 넘기면 React가 커밋 단계에서 실제 DOM 노드를 current에 넣어준다.
- 렌더current 는 아직 null
- 커밋DOM 이 만들어진다
- ref.current = 노드React 가 넣어준다
- useEffect이제 쓸 수 있다
렌더 중에는 null이다. 15강에서 본 실행 순서를 떠올리면 당연하다. DOM이 아직 없는데 담아줄 수가 없다.
언제 DOM 을 직접 만지나
React가 못 하는 일에만 쓴다.
| 써도 되는 일 | 쓰면 안 되는 일 |
|---|---|
| 포커스 주기 (focus) | 텍스트 바꾸기 |
| 스크롤 위치 조정 | 요소 추가 · 삭제 |
| 요소 크기 재기 | 클래스 붙이기 |
| 미디어 재생 (play · pause) | 스타일 바꾸기 |
| 외부 라이브러리에 노드 넘기기 | 화면에 보이는 것 조작 |
오른쪽 것들은 전부 state로 해결된다. 1강의 선언형을 버리는 순간 데이터와 화면이 어긋나기 시작한다.
function Bad({ isActive }) {
const ref = useRef(null);
useEffect(() => {
if (isActive) ref.current.classList.add("active");
else ref.current.classList.remove("active");
}, [isActive]);
return <div ref={ref}>…</div>;
}
function Good({ isActive }) {
return <div className={isActive ? "active" : ""}>…</div>;
}
아래가 짧고, 안전하고, React의 방식이다.
자식의 DOM 을 받는다
부모가 자식 안의 input에 포커스를 주고 싶다면 ref를 넘긴다.
function TextInput({ ref }) {
return <input ref={ref} />;
}
function Form() {
const inputRef = useRef(null);
return (
<>
<TextInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>입력창으로</button>
</>
);
}
React 19부터 ref를 그냥 props처럼 받을 수 있다. 그 전에는 forwardRef로 감싸야 했다. 옛 코드에서 forwardRef를 보면 이 이야기라고 생각하면 된다.
이전 값을 기억한다
ref가 리렌더링을 일으키지 않는다는 성질을 이용한 패턴이다.
function Counter({ count }) {
const prevCount = useRef(count);
useEffect(() => {
prevCount.current = count;
}, [count]);
return (
<p>
{count} (이전: {prevCount.current})
</p>
);
}
렌더링 중에는 아직 useEffect가 안 돌았으므로 prevCount.current에 이전 값이 남아 있다. effect가 실행되면서 새 값으로 갱신된다.
정리하면
useRef는 렌더링을 일으키지 않고 값을 기억한다.{ current: 값 }상자를 준다.- 그 상자는 렌더링이 반복돼도 같은 상자다.
- 판단 기준은 하나다. 바뀌면 화면이 바뀌어야 하는가. 그렇다면 state, 아니면 ref.
- ref를 화면에 그리지 않는다. 값이 바뀌어도 화면이 안 바뀐다.
- 렌더 중에
ref.current를 읽거나 쓰지 않는다. 순수성이 깨지고, DOM ref는 아직null이다. - DOM은 React가 못 하는 일에만 직접 만진다. 포커스·스크롤·크기 측정 정도다.
- 텍스트·클래스·스타일은 state로 한다. 직접 만지면 1강의 문제가 돌아온다.
다음 강의에서는 여러 컴포넌트가 같은 값을 나눠 쓰는 법, 상태 끌어올리기와 Context를 본다. Part 5의 마지막이다.