리액트 useRef 사용법: DOM 제어, input focus, 이전 값 저장하기
useRef는 렌더링 사이에 값을 보존하거나, 실제 DOM 요소에 직접 접근해 포커스·스크롤·스타일을 제어할 때 쓰는 훅입니다. ref.current 값을 바꿔도 리렌더링은 일어나지 않으므로 화면에 보여줄 값은 여전히 useState로 관리하는 것이 원칙입니다.
1. useRef 핵심 요약
- 형태:
const elRef = useRef(null)→elRef.current에 DOM 노드(또는 임의의 값) 저장 - 리렌더 X:
ref.current변경은 리렌더를 트리거하지 않음(성능 이점) - 주요 용도: 포커스/스크롤/선택 제어, 외부 라이브러리 핸들 보관, 이전 값/타이머 ID 저장
2. 예제 1 — DOM 직접 제어(스크롤·하이라이트)
버튼을 클릭하면 지정 박스로 스무스 스크롤되고, 테두리가 잠깐 하이라이트됩니다. ref.current를 통해 스타일을 직접 변경합니다.
import React, { useRef } from 'react';
function DomControlDemo() {
const boxRef = useRef(null);
const scrollToBox = () => {
boxRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
};
const highlight = () => {
const el = boxRef.current;
if (!el) return;
el.style.transition = 'box-shadow 300ms';
el.style.boxShadow = '0 0 0 3px #3b82f6 inset';
setTimeout(() => { if (el) el.style.boxShadow = 'none'; }, 700);
};
return (
<section style={{border:'1px solid #ddd', padding:12, borderRadius:8, height:600, overflowY:'auto'}}>
<h4>DomControlDemo 예제</h4>
<p>버튼으로 대상 박스로 스크롤하고, 테두리를 잠시 하이라이트합니다.</p>
<div style={{display:'flex', gap:8, marginBottom:12}}>
<button onClick={scrollToBox}>해당 영역으로 스크롤</button>
<button onClick={highlight}>하이라이트</button>
</div>
{/* 스크롤 공간용 플레이스홀더 */}
{Array.from({ length: 4 }).map((_, i) => <div key={i} style={{ height: 80 }} />)}
<div
ref={boxRef}
style={{
height:160,
background:'#f8fafc',
border:'1px solid #94a3b8',
borderRadius:8,
display:'grid',
placeItems:'center',
fontWeight:'bold'
}}
>
타깃 상자 (ref 대상)
</div>
{Array.from({ length: 8 }).map((_, i) => <div key={i} style={{ height: 80 }} />)}
</section>
);
}
export default DomControlDemo;▼ 실행 화면 설명: 스크롤 컨테이너 상단의 버튼을 누르면 화면이 부드럽게 아래 ‘타깃 상자’로 이동합니다. “하이라이트”를 누르면 상자 내부 테두리가 잠시 파란색으로 반짝입니다.

3. 예제 2 — input focus 제어(자동 포커스·Enter로 다음 이동)
첫 렌더링 시 이름 입력란에 자동 포커스가 가고, Enter를 누르면 다음 입력으로 포커스가 이동합니다.
import React, { useEffect, useRef } from 'react';
function InputFocusDemo() {
const nameRef = useRef(null);
const emailRef = useRef(null);
// 마운트 시 첫 입력창 자동 포커스
useEffect(() => {
nameRef.current?.focus();
}, []);
const focusNext = () => emailRef.current?.focus();
return (
<section style={{border:'1px solid #ddd', padding:12, borderRadius:8}}>
<h4>InputFocusDemo 예제</h4>
<p>첫 렌더링 시 이름 입력창에 포커스가 자동으로 이동하고, Enter 입력 시 이메일 입력창으로 넘어갑니다.</p>
<label>이름</label>
<input
ref={nameRef}
placeholder="이름 입력"
onKeyDown={(e) => { if (e.key === 'Enter') focusNext(); }}
style={{display:'block', width:'100%', marginBottom:8}}
/>
<label>이메일</label>
<input
ref={emailRef}
placeholder="email@example.com"
style={{display:'block', width:'100%', marginBottom:8}}
/>
<button onClick={focusNext}>다음 입력으로 포커스</button>
</section>
);
}
export default InputFocusDemo;▼ 실행 화면 설명: 컴포넌트가 나타나자마자 이름 입력창 커서가 깜박입니다. 이름 입력 후 Enter를 누르거나 버튼을 클릭하면 이메일 입력창으로 포커스가 이동합니다.

4. 예제 3 — 이전 값 저장(렌더 간 값 비교)
입력할 때마다 이전 렌더에서의 값을 useRef에 저장해 현재 값과 나란히 보여줍니다. 상태(State)는 화면 표시용, ref는 비교·기록용으로 분리합니다.
import React, { useEffect, useRef, useState } from 'react';
function PreviousValueDemo() {
const [text, setText] = useState('');
const prevTextRef = useRef(text); // 초기값을 현재 값으로 저장
// text가 바뀔 때마다 '이전 값 저장소' 갱신
useEffect(() => {
prevTextRef.current = text;
}, [text]);
// 리렌더 없이 누적 카운트 저장 (UI 보조 정보용)
const changeCountRef = useRef(0);
useEffect(() => {
changeCountRef.current += 1;
}, [text]);
return (
<section style={{border:'1px solid #ddd', padding:12, borderRadius:8}}>
<h4>PreviousValueDemo 예제</h4>
<p>입력할 때마다 현재 값과 이전 값을 나란히 보여주고, 변경 횟수는 ref에 누적됩니다.</p>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="아무 내용이나 입력"
style={{display:'block', width:'100%', marginBottom:12}}
/>
<div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:12}}>
<div style={{background:'#f8fafc', padding:8, borderRadius:6}}>
<strong>현재 값</strong>
<pre style={{margin:0}}>{text || '(빈 문자열)'}</pre>
</div>
<div style={{background:'#fff7ed', padding:8, borderRadius:6}}>
<strong>이전 값 (ref)</strong>
<pre style={{margin:0}}>{prevTextRef.current || '(초기값)'}</pre>
</div>
</div>
<p style={{marginTop:8, color:'#64748b'}}>
변경 횟수(리렌더 없이 ref로 누적): {changeCountRef.current}
</p>
<hr/>
<pre style={{background:'#f6f8fa', padding:8}}>
{JSON.stringify(
{ current: text, previous: prevTextRef.current, changes: changeCountRef.current },
null,
2
)}
</pre>
</section>
);
}
export default PreviousValueDemo;▼ 실행 화면 설명: 입력 상자 아래 ‘현재 값’과 ‘이전 값’이 나란히 표시됩니다. 타이핑할 때마다 이전 렌더의 값을 정확히 비교할 수 있습니다. 변경 횟수는 ref로 누적되며, 이 값 자체는 리렌더를 유발하지 않습니다.

5. 자주 하는 실수 & 베스트 프랙티스
- 화면 표시값은 state로:
ref.current변경만으로는 화면이 갱신되지 않습니다. UI에 보여줄 데이터는useState. - DOM 조작은 최소화: 가능하면 선언적 방식(조건부 렌더링·스타일 바인딩) 우선, 꼭 필요할 때만 ref로 직접 제어.
- 이벤트 리스너/외부 핸들 보관: 등록한 핸들러/타이머 ID/외부 인스턴스는 ref에 보관하고, 정리는
useEffect의 cleanup에서 수행. - Strict Mode 이중 호출 이슈: 개발 모드에서 효과/마운트가 두 번처럼 보일 수 있으나 ref 저장 자체는 문제 없습니다(React 18 개발모드 특성).
참고 링크
- React 공식 문서: Referencing Values with Refs
- React 공식 문서: Manipulating the DOM with Refs
- React API: useRef
함께 보면 좋은 게시글
- React JSX 기초: 표현식·조건부 렌더링·리스트 출력
- 리액트 JSX 문법 규칙: 인라인 스타일링·className·닫는 태그·주석
- 리액트 props 사용법: 부모에서 자식으로 데이터 전달하기
- 리액트 state 관리: useState 기본 사용법과 동작 원리
- 리액트 useEffect 사용법: 의존성 배열과 생명주기 이해
이 글이 도움이 되셨다면 공유 부탁 드립니다.



