useRef란?React에서 useRef는 두 가지 주요 목적으로 사용되는 훅(Hook)임
렌더링 사이에 값을 유지할 때
useState와 달리 값이 바뀌어도 컴포넌트를 다시 렌더링하지 않으면서, 값이 여러 번의 렌더링 사이에도 사라지지 않고 유지되도록 하고 싶을 때 사용함DOM 요소에 직접 접근할 때
<input>, <button>, <div> 등)에 직접 접근해서useRef를 호출하면 { current: 초기값 } 형태의 객체를 반환함current 속성에 저장된 값은 변경해도 컴포넌트가 다시 렌더링되지 않음<T>와 같은 형태로 사용됨useRef의 제네릭 타입 <T>useRef는 내부적으로 다음처럼 정의되어 있음
function useRef<T>(initialValue: T): MutableRefObject<T>
➡️ 즉, useRef<T>()처럼 타입 T를 직접 지정할 수 있는 제네릭 함수임
useRef(null)만 쓰면 타입이 null로 고정돼서 current.value 같은 속성을 쓸 수 없음❌ 문제 예시: 타입 지정을 안 한 경우
const inputRef = useRef(null); // 타입: null
inputRef.current.focus(); // 오류: focus가 없다고 나옴
✅ 해결 예시: 제네릭으로 타입 지정
const inputRef = useRef<HTMLInputElement>(null);
return <input ref={inputRef} />;
// 이제 타입스크립트가 inputRef.current를 HTMLInputElement로 인식
inputRef.current?.focus();
✅ 예시 1: 버튼에 접근하고 싶을 때
const buttonRef = useRef<HTMLButtonElement | null>(null);
return <button ref={buttonRef}>Click</button>;
✅ 예시 2: input 요소에 포커스
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return <input ref={inputRef} />;
✅ 예시 3: 리렌더링과 무관하게 값을 유지하고 싶을 때
const intervalId = useRef<number | null>(null);
useEffect(() => {
intervalId.current = window.setInterval(() => {
console.log("타이머 동작 중");
}, 1000);
return () => {
if (intervalId.current !== null) {
clearInterval(intervalId.current);
}
};
}, []);
✅ 예시 4: 사용자 정의 타입으로 값 저장
interface UserInfo {
name: string;
age: numver;
}
const userRef = useRef<UserInfo>({ name: "Hyunjin", age: 25 });
console.log(userRef.current.name); // "Hyunjin"
useRef<HTMLInputElement | null>(null)와 React.RefObject<HTMLInputElement>의 관계useRef<HTMLInputElement | null>(null)
리액트에서 useRef()는 값을 기억하거나, DOM 요소를 직접 가리키는 참조(ref)를 만들 때 사용하는 훅임
const inputRef = useRef<HTMLInputElement | null>(null);
inputRef는 ref 객체가 됨.current라는 속성이 있고, 그 .current 속성에 진짜 <input> DOM 요소가 들어감<input ref={inputRef} />➡️ 이렇게 연결하면, 나중에 inputRef.current?.focus()로 입력창에 자동 포커스를 줄 수 있음useRef<HTMLInputElement | null>(null)의 리턴 타입은?
사실 useRef()는 내부적으로 이렇게 정의되어 있음
function useRef<T>(initialValue: T): React.RefObject<T>
그래서 useRef<HTMLInputElement | null>(null)를 쓰면 타입스크립트가 자동으로 다음과 같이 인식함
const inputRef: React.RefObject<HTMLInputElement>
➡️ 즉 우리가 명시적으로 React.RefObject라고 쓰지 않아도, TypeScript가 알아서 추론해서 적용해줌
그럼 React.RefObject<HTMLInputElement>는 언제 쓸까?
이건 ref를 함수에 전달할 때, 매개변수 타입을 선언하고 싶을 때 쓰는 타입임
// 어떤 ref를 받아서 내부에서 사용하는 함수
function focusInput(ref: React.RefObject<HTMLInputElement>) {
ref.current?.focus();
}
➡️ 이때 focusInput() 함수가 받을 수 있는 ref의 타입을 선언한 것임
역할 비교
| 쓰임새 | 예시 코드 | 설명 |
|---|---|---|
| ref 객체를 생성할 때 | const inputRef = useRef<HTMLInputElement \| null>(null) | ref 객체를 직접 생성하고 DOM 요소에 연결할 때 사용 |
| ref 객체를 타입으로 선언하고 싶을 때 | function handle(ref: React.RefObject<HTMLInputElement>) | 다른 함수나 컴포넌트에 ref를 전달할 때 타입을 명시함 |
실전 비유
useRef<HTMLInputElement | null>(null)는 주소를 하나 만드는 행위null이지만, 나중에 실제 HTML 요소의 위치를 기억하게 됨React.RefObject<HTMLInputElement>는 "이건 주소 객체야!"라고 타입 선언을 하는 행위달력 아이콘(<img>)을 눌렀을 때, 인풋 필드에 달력 팝업을 띄우기 위해서 useRef를 사용하였다.

인풋 필드에 직접 접근하기 위한 참조(Ref)를 만드는 코드
const startDateRef = useRef<HTMLInputElement | null>(null);
const endDateRef = useRef<HTMLInputElement | null>(null);
useRef는 컴포넌트가 렌더링되어도 값이 유지되는 참조 객체를 생성함
여기서는 <input> 요소에 대한 참조를 만들기 때문에, 제네릭 타입으로 HTMLInputElement를 명시함
null은 초기값으로, 렌더링 직전에 이 ref는 아직 DOM을 참조하지 못하기 때문에 null로 시작함
startDateRef.current와 endDateRef.current는 나중에 <input> DOM을 가리킴💖 React 컴포넌트의 렌더링 흐름: 초기 렌더링 시
- 컴포넌트 함수가 호출됨
useRef(null)실행됨 →.current는 여전히null- JSX가 반환되어 가상 DOM이 구성됨
- 이후에 실제 DOM에 컴포넌트가 마운트됨
- 그리고 나서
ref={startDateRef}가 연결된 DOM 요소를startDateRef.current가 참조하게 됨➡️ 초기 렌더링이 끝나고 나서야
ref.current가 진짜 DOM 요소를 가지게 됨
인풋 필드에 직접 접근하기 위해 <input type="date"> 태그에 참조(Ref)를 연결한 코드
<input
type="date"
value={travelPeriod.startDate}
onChange={handleDateChange("startDate")}
ref={startDateRef} // 👈 이 부분 주목
min={today}
/><input
type="date"
value={travelPeriod.endDate}
onChange={handleDateChange("endDate")}
ref={endDateRef} // 👈 이 부분 주목
min={travelPeriod.startDate || today}
/>
커스텀 달력 아이콘 클릭 시 <input> 태그의 달력 팝업을 강제로 여는 함수(handleIconClick)
const handleIconClick = (ref: React.RefObject<HTMLInputElement> | null) => {
ref?.current?.showPicker?.(); // 일부 브라우저 지원
ref?.current?.focus(); // 브라우저 호환성을 위해 fallback
};
ref?.current?.showPicker?.()
showPicker() : 일부 최신 브라우저(Chrome 114+ 등)에서 제공하는 메서드<input type="date">에서 달력 UI를 프로그래밍적으로 여는 기능?. : 옵셔널 체이닝 → ref.current가 존재하고, showPicker가 있으면 실행한다는 의미ref?.current?.focus()
showPicker()를 지원하지 않는 경우를 대비한 대체용<input type="date">에 focus()를 주면 자동으로 달력 팝업이 열림.focus()는 fallback(원래 쓰려던 방법이 안 될 때 대신 사용하는 대체 수단) 역할을 함달력 아이콘을 가리키는 <img> 태그에 <input>의 달력 팝업을 강제로 여는 함수(handleIconClick)를 이벤트 핸들러로 연결한 코드
<img
src="/images/calendar.svg"
alt="calendar"
onClick={() => handleIconClick(startDateRef)} // 👈 이 부분 주목
/><img
src="/images/calendar.svg"
alt="calendar"
onClick={() => handleIconClick(endDateRef)} // 👈 이 부분 주목
/>🚨 하지만 위와 같이 코드를 작성했을 때 다음과 같은 타입 에러가 발생하였다.

핵심 에러 메시지
Argument of type '
RefObject<HTMLInputElement | null>' is not assignable to parameter of type 'RefObject<HTMLInputElement>'
useRef의 성질
useRef()는 상황에 따라 MutableRefObject<T> 또는 RefObject<T>를 반환함useRef의 초기값으로 null을 넣었을 때 나오는 RefObject<T | null>은 React에서 JSX ref prop 용으로 따로 지정한 특별한 오버로드임
useRef의 타입 정의 (index.d.ts)
index.d.ts 파일을 보면, 이렇게 3가지 오버로드로 되어 있음
// 1. 일반적인 값 (비 DOM ref 용도 등)
function useRef<T>(initialValue: T): MutableRefObject<T>;
// 2. JSX에서 ref prop에 전달되는 null 초기값용
function useRef<T>(initialValue: T | null): RefObject<T | null>;
// 3. undefined 버전
function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
각각이 어떤 상황에서 호출될까?
| 사용 예 | 선택되는 오버로드 | 반환 타입 |
|---|---|---|
| useRef(0) | 첫 번째 오버로드 | MutableRefObject<number> |
useRef<HTMLInputElement>(null) | 두 번째 오버로드 | RefObject<HTMLInputElement | null> |
| useRef<string | undefined>(undefined) | 세 번째 오버로드 | RefObject<string | undefined> |
useRef(null)과 같이 선언하였을 때 RefObject가 되는 이유
ref={...}로 전달할 때 readonly 타입이 더 안전하기 때문임useRef(null) 같은 경우에 RefObject<T | null>을 반환하도록 특별히 오버로드를 제공했기 때문임핵심 포인트
RefObject<T>지만, React의 실제 구현체에서는 이 ref 객체는 여전히 mutable함ref.current = someValue; // 가능함함수 인자 타입 안 맞는 이유
RefObject<T> ≠ MutableRefObject<T> 또는 RefObject<A> ≠ RefObject<B> 때문💡 에러 코드 수정
handleIconClick의 매개변수를 HTMLInputElement로 직접 받는 것으로 수정
const handleIconClick = (inputEl: HTMLInputElement | null) => { // 👈 이 부분 주목
inputEl?.showPicker?.();
inputEl?.focus();
};
<img> 태그의 onClick에서 매개변수로 .current를 넘기는 것으로 수정
<img
src="/images/calendar.svg"
alt="calendar"
onClick={() => handleIconClick(startDateRef.current)}/> // 👈 이 부분 주목
startDateRef.current는 실제 DOM 엘리먼트이기 때문에 타입 충돌이 없음 (매개변수와 타입 일치)startDateRef.current 타입은 HTMLInputElement | null임React.RefObject<T>는 TypeScript에 다음과 같이 정의되어 있음interface RefObject<T> {
readonly current: T | null;
} ➡️ 즉, RefObject<T>를 사용하면 current의 타입은 항상 T | null이 됨