React 독학 가이드 10 - useRef로 변수 생성하기
useState 말고 값을 저장하는 방법이 하나 더 있어요. 바로 useRef입니다. 근데 useState랑 뭐가 다르냐고요?
useRef vs useState
핵심 차이:
useState: 값이 바뀌면 컴포넌트가 다시 렌더링됨useRef: 값이 바뀌어도 렌더링 안 됨
언제 useRef를 쓸까요?
- DOM 요소에 직접 접근할 때
- 렌더링 없이 값을 저장하고 싶을 때
- 이전 값을 기억하고 싶을 때
useRef 기본 사용법
import { useRef } from 'react';
function MyComponent() {
const myRef = useRef(초기값);
console.log(myRef.current); // 현재 값 접근
myRef.current = 새로운값; // 값 변경 (렌더링 안 됨!)
}
useRef는 .current 속성을 가진 객체를 반환해요.
활용 1: DOM 요소 접근하기
useRef의 가장 흔한 용도예요. input에 자동 포커스를 주는 예시를 볼게요.
자동 포커스
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// 컴포넌트가 마운트되면 input에 포커스
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="자동 포커스됨!" />;
}
실행 결과:
▲ 페이지 로드 시 자동으로 포커스됩니다
useRef로 DOM 요소에 접근하여 focus() 메서드를 호출합니다
ref={inputRef}로 DOM 요소를 연결하면, inputRef.current로 그 요소에 접근할 수 있어요.
스크롤 위치 제어
import { useRef } from 'react';
function ScrollExample() {
const topRef = useRef(null);
const bottomRef = useRef(null);
const scrollToTop = () => {
topRef.current.scrollIntoView({ behavior: 'smooth' });
};
const scrollToBottom = () => {
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
};
return (
<div>
<div ref={topRef}>
<h1>맨 위</h1>
<button onClick={scrollToBottom}>맨 아래로 이동</button>
</div>
{/* 긴 콘텐츠 */}
<div style={{ height: '1000px', background: '#f0f0f0' }}>
<p>스크롤 해보세요...</p>
</div>
<div ref={bottomRef}>
<h1>맨 아래</h1>
<button onClick={scrollToTop}>맨 위로 이동</button>
</div>
</div>
);
}
비디오 제어
import { useRef } from 'react';
function VideoPlayer() {
const videoRef = useRef(null);
const handlePlay = () => {
videoRef.current.play();
};
const handlePause = () => {
videoRef.current.pause();
};
const handleRestart = () => {
videoRef.current.currentTime = 0;
videoRef.current.play();
};
return (
<div>
<video
ref={videoRef}
src="video.mp4"
style={{ width: '100%' }}
/>
<div>
<button onClick={handlePlay}>재생</button>
<button onClick={handlePause}>일시정지</button>
<button onClick={handleRestart}>처음부터</button>
</div>
</div>
);
}
활용 2: 렌더링 없이 값 저장하기
클릭 횟수 저장 (렌더링 없이)
import { useRef } from 'react';
function ClickCounter() {
const clickCount = useRef(0); const handleClick = () => {
clickCount.current += 1;
console.log(`클릭 횟수: ${clickCount.current}`);
// 화면은 업데이트 안 됨!
};
return (
<div>
<button onClick={handleClick}>클릭하세요</button>
<p>콘솔을 확인하세요 (화면 숫자는 안 바뀜)</p>
</div>
);
}
화면에 보여줄 필요 없고 내부적으로만 추적하고 싶을 때 유용해요.
이전 값 기억하기
import { useState, useRef, useEffect } from 'react';
function PreviousValue() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0);
useEffect(() => {
// 렌더링 후에 현재 값을 저장
prevCountRef.current = count;
}, [count]);
return (
<div>
<p>현재: {count}</p>
<p>이전: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
타이머/인터벌 ID 저장
import { useState, useRef } from 'react';
function Stopwatch() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);
const start = () => {
if (isRunning) return;
setIsRunning(true);
intervalRef.current = setInterval(() => {
setTime((prev) => prev + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
setIsRunning(false);
};
const reset = () => {
clearInterval(intervalRef.current);
setTime(0);
setIsRunning(false);
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
return (
<div style={{ textAlign: 'center' }}>
<h1 style={{ fontSize: '48px' }}>{formatTime(time)}</h1>
<button onClick={start} disabled={isRunning}>시작</button>
<button onClick={stop} disabled={!isRunning}>정지</button>
<button onClick={reset}>리셋</button>
</div>
);
}
실행 결과:
01:23
useRef로 타이머 ID를 저장하여 시작/정지/리셋 기능을 구현합니다
intervalRef로 인터벌 ID를 저장해서 나중에 clearInterval로 정지할 수 있어요.
실전 예제: 입력 포커스 관리
import { useRef, useState } from 'react';
function LoginForm() {
const [form, setForm] = useState({ username: '', password: '' });
const [errors, setErrors] = useState({});
const usernameRef = useRef(null);
const passwordRef = useRef(null);
const handleChange = (e) => {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = {};
if (!form.username) {
newErrors.username = '아이디를 입력하세요';
usernameRef.current.focus(); // 에러난 필드로 포커스!
} else if (!form.password) {
newErrors.password = '비밀번호를 입력하세요';
passwordRef.current.focus();
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
console.log('로그인 성공!', form);
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
ref={usernameRef}
name="username"
value={form.username}
onChange={handleChange}
placeholder="아이디"
style={{ borderColor: errors.username ? 'red' : '#ddd' }}
/>
{errors.username && <span style={{ color: 'red' }}>{errors.username}</span>}
</div>
<div>
<input
ref={passwordRef}
name="password"
type="password"
value={form.password}
onChange={handleChange}
placeholder="비밀번호"
style={{ borderColor: errors.password ? 'red' : '#ddd' }}
/>
{errors.password && <span style={{ color: 'red' }}>{errors.password}</span>}
</div>
<button type="submit">로그인</button>
</form>
);
}
실전 예제: 캔버스 그리기
import { useRef, useEffect, useState } from 'react';
function DrawingCanvas() {
const canvasRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
const lastPos = useRef({ x: 0, y: 0 });
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#000';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
}, []);
const startDrawing = (e) => {
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
lastPos.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
setIsDrawing(true);
};
const draw = (e) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
const currentPos = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
ctx.beginPath();
ctx.moveTo(lastPos.current.x, lastPos.current.y);
ctx.lineTo(currentPos.x, currentPos.y);
ctx.stroke();
lastPos.current = currentPos;
};
const stopDrawing = () => {
setIsDrawing(false);
};
const clearCanvas = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
return (
<div>
<canvas
ref={canvasRef}
width={400}
height={300}
style={{ border: '2px solid #333', cursor: 'crosshair' }}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
/>
<br />
<button onClick={clearCanvas}>지우기</button>
</div>
);
}
실전 예제: 무한 스크롤 감지
import { useRef, useEffect, useState } from 'react';
function InfiniteScroll() {
const [items, setItems] = useState(Array.from({ length: 20 }, (_, i) => i + 1));
const [loading, setLoading] = useState(false);
const observerRef = useRef(null);
const loadMore = () => {
setLoading(true);
// 데이터 로딩 시뮬레이션
setTimeout(() => {
setItems((prev) => [
...prev,
...Array.from({ length: 10 }, (_, i) => prev.length + i + 1)
]);
setLoading(false);
}, 1000);
};
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !loading) {
loadMore();
}
},
{ threshold: 1.0 }
);
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => observer.disconnect();
}, [loading]);
return (
<div style={{ height: '400px', overflow: 'auto' }}>
{items.map((item) => (
<div
key={item}
style={{
padding: '20px',
borderBottom: '1px solid #ddd'
}}
>
아이템 {item}
</div>
))}
<div ref={observerRef} style={{ padding: '20px', textAlign: 'center' }}>
{loading ? '로딩 중...' : '더 보기'}
</div>
</div>
);
}
useRef 사용 시 주의사항
1. 렌더링 중에 current를 읽거나 쓰지 마세요
// ❌ 안 좋음
function BadComponent() {
const ref = useRef(0);
ref.current += 1; // 렌더링 중에 변경!
return <p>{ref.current}</p>; // 렌더링 중에 읽기!
}
// ⭕ 좋음 - 이벤트 핸들러나 useEffect 안에서 사용
function GoodComponent() {
const ref = useRef(0);
const handleClick = () => {
ref.current += 1;
console.log(ref.current);
};
return <button onClick={handleClick}>클릭</button>;
}
2. DOM ref는 null일 수 있음
function SafeRef() {
const divRef = useRef(null);
const handleClick = () => {
// null 체크!
if (divRef.current) {
divRef.current.style.backgroundColor = 'red';
}
};
return <div ref={divRef}>내용</div>;
}
3. 화면에 보여줄 값은 useState를 사용하세요
// ❌ 화면이 안 바뀜
const count = useRef(0);
return <p>{count.current}</p>; // 클릭해도 화면 그대로
// ⭕ 화면이 바뀜
const [count, setCount] = useState(0);
return <p>{count}</p>; // 클릭하면 화면 업데이트
useRef vs useState 정리
| 상황 | 사용할 Hook |
|---|---|
| 화면에 보여줘야 함 | useState |
| DOM 요소 접근 | useRef |
| 화면 업데이트 없이 값 저장 | useRef |
| 타이머/인터벌 ID 저장 | useRef |
| 이전 값 기억 | useRef |
다음 단계
useRef를 배웠으니, 다음 글에서는 React의 Hooks 전체를 정리해볼게요. useState, useRef, useEffect, useMemo, useCallback 등 주요 Hooks를 한눈에 볼 수 있게 정리해드릴게요.
useRef는 처음엔 언제 써야 할지 애매한데, 몇 번 써보면 “아, 이럴 때 쓰는 거구나” 감이 와요!
React 시리즈 탐색:
← 블로그 목록으로