useRef 개요 DOM 접근 값 보관 훅의 규칙 기타 훅 📝 문제풀이
◀ 이전 📋 목차 CH 10 ▶
📌 CHAPTER 09 · 중급

useRef — 리렌더 없이 붙잡아 두는 도구

useState가 "바뀌면 화면을 다시 그리는" 값이라면, useRef는 "화면을 다시 그리지 않고 붙잡아 두는" 값이에요. 여기에 실제 DOM 요소를 직접 만지는 능력까지 얹혀 있죠. 이번 장에서 useRef의 두 가지 쓰임과, 모든 훅에 공통으로 적용되는 훅의 규칙까지 차근차근 정리해요.

🎯 이 장을 끝내면
📌
useRef 개요
useRef의 두 가지 쓰임
이름은 하나지만, 쓰는 목적이 두 갈래예요. 먼저 큰 그림부터.

useRef는 리액트가 기본으로 주는 훅이에요. const 이름 = useRef(초깃값)으로 만들면, .current라는 칸 하나를 가진 상자를 돌려줘요. 이 상자는 딱 두 가지 상황에서 유용해요.

🔤
훅(hook) — 함수 컴포넌트 안에서 state·부수효과 같은 리액트 기능을 "끌어다 쓰게" 해주는 특수 함수예요. 이름이 모두 use로 시작해요(useState·useEffect·useRef…).
🎯
① DOM 직접 접근
input·div 같은 실제 요소를
손으로 만지기 (포커스 등)
·
📦
② 값 보관
리렌더를 일으키지 않고
값을 붙잡아 두기
📦 useRef가 주는 건 "current 칸 하나짜리 상자"🟪 리액트 const ref = useRef(0)이라고 쓰면 ref.current0이 들어 있어요. 우리는 이 .current를 읽거나 바꿔서 씁니다. 핵심은 이 상자가 리렌더가 일어나도 사라지지 않고 그대로 유지된다는 점, 그리고 .current를 바꿔도 화면을 다시 그리지 않는다는 점이에요. 지금은 "useRef = current 칸 하나짜리 상자, ① 실제 DOM 만지기 ② 리렌더 없이 값 보관"이라는 큰 그림만 챙기면 충분해요.
useRef .current ref 속성 DOM 접근 focus() 리렌더 안 함 값 유지 훅의 규칙
🧭
이번 장의 읽는 법. useRef는 "언제 state 대신 ref를 쓰나?"가 헷갈리는 훅이에요. sec-1·sec-2에서 두 쓰임을 하나씩 예제로 보고, sec-2 끝의 비교표로 "언제 ref, 언제 state"를 딱 정리하세요.
🎯
DOM 접근
실제 DOM 요소를 직접 만지기🟦 순수 JS
입력창에 포커스 주기가 대표적인 예예요.

리액트는 보통 화면을 선언형으로 그려요. 우리가 document.querySelector로 요소를 찾아 직접 만질 일이 거의 없죠. 하지만 입력창에 포커스 주기, 스크롤 위치 옮기기, 비디오 재생/정지처럼 진짜 DOM 요소를 직접 조작해야 하는 경우가 있어요. 이럴 때 useRef를 씁니다.

🔤
DOM — 브라우저가 화면에 그린 실제 요소들의 구조예요. 선언형 — "무엇을 보여줄지"만 state로 적으면 리액트가 알아서 그 DOM을 맞춰 그리는 방식이고, document.querySelector로 요소를 직접 찾아 하나씩 지시하는 건 명령형이에요.

3단계로 연결해요 — ① useRef(null)로 ref를 만들고, ② JSX 요소에 ref={...}로 붙이고, ③ 필요할 때 ref.current로 실제 DOM에 접근합니다.

📦
① 상자 만들기
useRef(null)
current = null
🔗
② 요소에 붙이기
<input ref={inputRef} />
🎯
③ current로 접근
(그려진 뒤)
inputRef.current.focus()
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의 두 번째 얼굴이에요. state와 대비해서 봐요.

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>
  );
}
⚠️
그럼 화면에 보여줄 값은? ref는 바꿔도 리렌더가 안 되므로, 화면에 그대로 반영돼야 하는 값은 반드시 state로 관리하세요. 위 예제에서 count가 state인 이유예요. ref는 "화면 밖에서 조용히 기억할 값"용이에요.
구분useState (상태)useRef (참조)
값 바꾸는 법setState(값)ref.current = 값
바꾸면 리렌더?일어남안 일어남
렌더돼도 값 유지?✅ 유지✅ 유지
화면에 보여줄 값여기에❌ 부적합
언제 쓰나화면에 반영돼야 하는 값DOM 접근 / 화면 밖 값 기억
(타이머 ID, 이전 값 등)
🧭
한 줄 판단법. "이 값이 바뀌면 화면이 달라져야 하나?" → 예면 state, 아니면(그냥 기억만) ref. 그리고 "실제 DOM 요소를 만져야 하나?" → 예면 ref.
📏
훅의 규칙 (중요)
모든 훅이 지켜야 하는 2가지 규칙
useState·useEffect·useRef… 전부에 공통이에요. 안 지키면 버그가 나요.

지금까지 배운 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… 하는 식이죠. 만약 조건문 때문에 어떤 렌더에선 훅이 호출되고 어떤 렌더에선 건너뛰면, 순서가 어긋나 리액트가 상태를 엉뚱한 곳에 매칭해 버려요. 그래서 호출 순서가 항상 일정해야 합니다.
1️⃣
최상위에서만
조건·반복·중첩
함수 안 금지
+
2️⃣
컴포넌트/커스텀 훅 안에서만
일반 함수 안 금지
=
호출 순서 일정
상태가 올바르게
매칭됨
🛠️
실무 팁. 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 · reducer · action · dispatch — useReduceruseState의 사촌으로, 상태 변경 로직이 복잡하거나 여러 값을 함께 다룰 때 써요. action은 "무엇을 할지 적은 쪽지"(예: { type: 'increment' }), dispatch는 그 쪽지를 보내는 함수, reducer(이전 상태, action)을 받아 다음 상태를 계산해 돌려주는 함수예요. 즉 dispatch(action)을 부르면 리액트가 reducer를 실행해 새 상태를 만들어줘요. (문제풀이에 이름이 나오니 여기서 한 번 짚어둬요. 자세한 사용법은 더 뒤에서!)
💡
훅 이름 규칙. 리액트 훅은 모두 use로 시작해요(useState·useRef·useContext…). 우리가 직접 만드는 커스텀 훅도 관례상 use로 시작하게 이름 붙여요(예: useToggle).
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 개념결과)을 섞어 두었어요.
CH 10 🌐 Context — props 없이 전역으로 값을 공유하는 법