useState가 "바뀌면 화면을 다시 그리는" 값이라면, useRef는 "화면을 다시 그리지 않고 붙잡아 두는" 값이에요. 여기에 실제 DOM 요소를 직접 만지는 능력까지 얹혀 있죠. 이번 장에서 useRef의 두 가지 쓰임과, 모든 훅에 공통으로 적용되는 훅의 규칙까지 차근차근 정리해요.
ref={inputRef} + inputRef.current.focus()로 입력창에 포커스 같은 실제 DOM 조작을 할 수 있어요..current를 바꿔도 리렌더가 안 일어난다는 것과, state와의 차이를 설명할 수 있어요.useRef는 리액트가 기본으로 주는 훅이에요. const 이름 = useRef(초깃값)으로 만들면, .current라는 칸 하나를 가진 상자를 돌려줘요. 이 상자는 딱 두 가지 상황에서 유용해요.
use로 시작해요(useState·useEffect·useRef…).const ref = useRef(0)이라고 쓰면 ref.current에 0이 들어 있어요. 우리는 이 .current를 읽거나 바꿔서 씁니다. 핵심은 이 상자가 리렌더가 일어나도 사라지지 않고 그대로 유지된다는 점, 그리고 .current를 바꿔도 화면을 다시 그리지 않는다는 점이에요.
지금은 "useRef = current 칸 하나짜리 상자, ① 실제 DOM 만지기 ② 리렌더 없이 값 보관"이라는 큰 그림만 챙기면 충분해요.
리액트는 보통 화면을 선언형으로 그려요. 우리가 document.querySelector로 요소를 찾아 직접 만질 일이 거의 없죠. 하지만 입력창에 포커스 주기, 스크롤 위치 옮기기, 비디오 재생/정지처럼 진짜 DOM 요소를 직접 조작해야 하는 경우가 있어요. 이럴 때 useRef를 씁니다.
document.querySelector로 요소를 직접 찾아 하나씩 지시하는 건 명령형이에요.3단계로 연결해요 — ① useRef(null)로 ref를 만들고, ② JSX 요소에 ref={...}로 붙이고, ③ 필요할 때 ref.current로 실제 DOM에 접근합니다.
import { useRef } from 'react';
function SearchBox() {
// ① current가 null인 상자를 만든다
const inputRef = useRef(null);
function handleClick() {
// ③ ref.current === 실제 <input> DOM 요소
inputRef.current.focus();
}
return (
<div>
{/* ② ref를 input에 붙인다 */}
<input ref={inputRef} placeholder="검색어" />
<button onClick={handleClick}>검색창으로 이동</button>
</div>
);
}
ref.current. <input ref={inputRef} />라고 붙이면, 화면에 그려진 뒤 리액트가 그 실제 <input> DOM 요소를 inputRef.current에 넣어줘요. 그래서 inputRef.current.focus()가 바로 그 입력창에 포커스를 주는 거예요.null일까🟪 리액트
useRef(null)로 시작하는 이유는, 컴포넌트 함수가 실행되는 시점엔 아직 실제 DOM이 화면에 그려지기 전이라서예요. 화면에 그려진(마운트된) 뒤에야 리액트가 inputRef.current에 실제 요소를 채워줍니다. 그래서 focus() 같은 조작은 보통 버튼 클릭·useEffect처럼 "그려진 다음" 시점에 해요.
순서를 기억하세요 — useRef(null) 만들기 → ref로 붙이기 → (그려진 뒤) current로 접근.
useRef는 DOM 말고 그냥 값을 담아 두는 용도로도 써요. 이때 특징이 두 가지예요.
.current를 바꿔도 리렌더가 안 일어나요🟪 리액트
ref.current = 5처럼 값을 바꿔도 화면을 다시 그리지 않아요. 반대로 setState는 값이 바뀌면 무조건 리렌더가 일어나죠. 그래서 "화면에 보여줄 필요는 없지만 어딘가 기억해 둬야 하는 값"은 ref가 딱이에요.
let count = 0)는 매번 초기화돼요. 하지만 ref의 .current는 렌더가 여러 번 일어나도 값이 그대로 살아 있어요. "리렌더돼도 사라지지 않는 서랍" 같은 거예요.
function Timer() {
const [count, setCount] = useState(0);
// 인터벌 ID처럼 "화면엔 안 보이지만 기억해 둘 값"
const timerId = useRef(null);
function start() {
// .current에 저장 — 리렌더 안 일어남
timerId.current = setInterval(() => {
setCount(c => c + 1); // 이건 state라 리렌더됨
}, 1000);
}
function stop() {
clearInterval(timerId.current); // 저장해 둔 값 재사용
}
return (
<div>
<p>{count}초</p>
<button onClick={start}>시작</button>
<button onClick={stop}>정지</button>
</div>
);
}
state로 관리하세요. 위 예제에서 count가 state인 이유예요. ref는 "화면 밖에서 조용히 기억할 값"용이에요.| 구분 | useState (상태) | useRef (참조) |
|---|---|---|
| 값 바꾸는 법 | setState(값) | ref.current = 값 |
| 바꾸면 리렌더? | ✅ 일어남 | ❌ 안 일어남 |
| 렌더돼도 값 유지? | ✅ 유지 | ✅ 유지 |
| 화면에 보여줄 값 | ✅ 여기에 | ❌ 부적합 |
| 언제 쓰나 | 화면에 반영돼야 하는 값 | DOM 접근 / 화면 밖 값 기억 (타이머 ID, 이전 값 등) |
지금까지 배운 useState·useEffect·useRef 같은 모든 훅에는 반드시 지켜야 하는 규칙이 있어요. 딱 두 개예요.
if)·반복문(for)·중첩 함수 안에서 훅을 호출하면 안 돼요. 훅은 항상 컴포넌트 함수의 맨 바깥(최상위)에서, 매번 같은 순서로 호출돼야 합니다.
function Bad({ show }) {
if (show) {
// ❌ 조건문 안에서 훅 호출 — 금지!
const [name, setName] = useState('');
}
// ...
}
function Good({ show }) {
// ✅ 최상위에서 항상 호출
const [name, setName] = useState('');
// 조건 분기는 훅 호출 "다음"에
if (show) { /* name 사용 */ }
// ...
}
use로 시작하는 커스텀 훅 안에서만 호출합니다.
useState, 두 번째 useState… 하는 식이죠. 만약 조건문 때문에 어떤 렌더에선 훅이 호출되고 어떤 렌더에선 건너뛰면, 순서가 어긋나 리액트가 상태를 엉뚱한 곳에 매칭해 버려요. 그래서 호출 순서가 항상 일정해야 합니다.eslint-plugin-react-hooks 규칙을 켜 두면, 이 규칙을 어겼을 때 에디터가 바로 경고해 줘요. Vite로 만든 프로젝트에는 보통 기본 설정돼 있어요.리액트에는 useState·useEffect·useRef 말고도 여러 훅이 있어요. 뒤 장에서 하나씩 제대로 다루니, 여기선 이름과 용도만 살짝 봐 둬요.
| 훅 | 용도 | 다루는 곳 |
|---|---|---|
useContext | 여러 컴포넌트가 공유하는 전역 값을 props 없이 꺼내 쓰기 | CH 10 |
useMemo | 무거운 계산 결과를 기억(캐시)해 성능 최적화 | CH 12 |
useCallback | 함수를 기억해 불필요한 재생성·리렌더 줄이기 | CH 12 |
useContext — props를 여러 단계 내려주는 게 번거로울 때, 전역으로 값을 공유해요. (CH 10에서 본격적으로!)useMemo / useCallback — 앱이 커지면 불필요한 재계산·재렌더가 생기는데, 이걸 줄이는 성능 최적화 훅이에요. (CH 12)
지금 단계에선 useState·useEffect·useRef 세 훅에 익숙해지는 게 우선이에요. 나머지는 필요해질 때 배우면 돼요.
useReducer는 useState의 사촌으로, 상태 변경 로직이 복잡하거나 여러 값을 함께 다룰 때 써요. action은 "무엇을 할지 적은 쪽지"(예: { type: 'increment' }), dispatch는 그 쪽지를 보내는 함수, reducer는 (이전 상태, action)을 받아 다음 상태를 계산해 돌려주는 함수예요. 즉 dispatch(action)을 부르면 리액트가 reducer를 실행해 새 상태를 만들어줘요. (문제풀이에 이름이 나오니 여기서 한 번 짚어둬요. 자세한 사용법은 더 뒤에서!)use로 시작해요(useState·useRef·useContext…). 우리가 직접 만드는 커스텀 훅도 관례상 use로 시작하게 이름 붙여요(예: useToggle)..current 칸 하나짜리 상자. 쓰임은 둘 — ① DOM 직접 접근, ② 리렌더 없이 값 보관.const r = useRef(null) → <input ref={r} /> → r.current.focus(). .current가 실제 DOM 요소..current는 바꿔도 리렌더가 안 일어나고, 리렌더돼도 값이 유지돼요. (state는 바뀌면 리렌더)