[TIL-0509] useRef의 제네릭 타입

jiny·2025년 5월 12일

캡스톤2

목록 보기
3/22

🌟 useRef란?

React에서 useRef는 두 가지 주요 목적으로 사용되는 훅(Hook)임

  1. 렌더링 사이에 값을 유지할 때

    • useState와 달리 값이 바뀌어도 컴포넌트를 다시 렌더링하지 않으면서, 값이 여러 번의 렌더링 사이에도 사라지지 않고 유지되도록 하고 싶을 때 사용함
    • 예를 들어, 컴포넌트가 마운트된 횟수이전 값을 저장해두고 싶을 때 유용함
  2. DOM 요소에 직접 접근할 때

    • 특정 HTML 요소(예: <input>, <button>, <div> 등)에 직접 접근해서
      1) 값을 읽거나
      2) 포커스를 주거나
      3) 애니메이션을 적용하는 등
      DOM API를 사용하고 싶을 때 사용함
  • useRef를 호출하면 { current: 초기값 } 형태의 객체를 반환함
  • 이 객체의 current 속성에 저장된 값은 변경해도 컴포넌트가 다시 렌더링되지 않음

🌟 제네릭(Generic)이란?

  • 다양한 타입을 유연하게 다룰 수 있으면서도 타입 안전성을 유지할 수 있게 해주는 기능
  • TypeScript에서 제네릭은 주로 <T>와 같은 형태로 사용됨
    • 여기서 T는 'Type'을 의미하며, 이 자리에 특정 타입을 넣어주겠다는 약속 같은 것임

🌟 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>의 관계

  1. useRef<HTMLInputElement | null>(null)
    리액트에서 useRef()값을 기억하거나, DOM 요소를 직접 가리키는 참조(ref)를 만들 때 사용하는 훅임

    const inputRef = useRef<HTMLInputElement | null>(null);
    • inputRefref 객체가 됨
    • 그 안에는 .current라는 속성이 있고, 그 .current 속성에 진짜 <input> DOM 요소가 들어감
      <input ref={inputRef} />
      ➡️ 이렇게 연결하면, 나중에 inputRef.current?.focus()로 입력창에 자동 포커스를 줄 수 있음
  2. 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가 알아서 추론해서 적용해줌

  3. 그럼 React.RefObject<HTMLInputElement>는 언제 쓸까?
    이건 ref를 함수에 전달할 때, 매개변수 타입을 선언하고 싶을 때 쓰는 타입임

    // 어떤 ref를 받아서 내부에서 사용하는 함수
    function focusInput(ref: React.RefObject<HTMLInputElement>) {
      ref.current?.focus();
    }

    ➡️ 이때 focusInput() 함수가 받을 수 있는 ref의 타입을 선언한 것임

  4. 역할 비교

    쓰임새예시 코드설명
    ref 객체를 생성할 때const inputRef = useRef<HTMLInputElement \| null>(null)ref 객체를 직접 생성하고 DOM 요소에 연결할 때 사용
    ref 객체를 타입으로 선언하고 싶을 때function handle(ref: React.RefObject<HTMLInputElement>)다른 함수나 컴포넌트에 ref를 전달할 때 타입을 명시
  5. 실전 비유

    • useRef<HTMLInputElement | null>(null)주소를 하나 만드는 행위
      ➡️ 처음에는 null이지만, 나중에 실제 HTML 요소의 위치를 기억하게 됨
    • React.RefObject<HTMLInputElement>는 "이건 주소 객체야!"라고 타입 선언을 하는 행위
      ➡️ 타입스크립트에 "이건 주소니까 current로 접근해도 돼!"라고 알려주는 것임

✍️ 구현 예시

  • 달력 아이콘(<img>)을 눌렀을 때, 인풋 필드에 달력 팝업을 띄우기 위해서 useRef를 사용하였다.

  • 인풋 필드에 직접 접근하기 위한 참조(Ref)를 만드는 코드

    const startDateRef = useRef<HTMLInputElement | null>(null);
    const endDateRef = useRef<HTMLInputElement | null>(null);
    • useRef컴포넌트가 렌더링되어도 값이 유지되는 참조 객체를 생성함

    • 여기서는 <input> 요소에 대한 참조를 만들기 때문에, 제네릭 타입으로 HTMLInputElement를 명시함

    • null은 초기값으로, 렌더링 직전에 이 ref는 아직 DOM을 참조하지 못하기 때문에 null로 시작함

      • startDateRef.currentendDateRef.current는 나중에 <input> DOM을 가리킴

      💖 React 컴포넌트의 렌더링 흐름: 초기 렌더링 시

      1. 컴포넌트 함수가 호출됨
      2. useRef(null) 실행됨 → .current는 여전히 null
      3. JSX가 반환되어 가상 DOM이 구성
      4. 이후에 실제 DOM에 컴포넌트가 마운트
      5. 그리고 나서 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>'

      • 이 에러는 TypeScript가 "ref의 current 속성이 null일 수 있음"이 아니라, "ref의 generic 타입 자체가 HTMLInputElement | null"이라고 보고 있기 때문에 발생함
    • 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가 되는 이유

        • JSX에서 ref={...}로 전달할 때 readonly 타입이 더 안전하기 때문임
        • React 팀이 JSX ref props일반 로컬 ref를 구분하기 위해 useRef(null) 같은 경우에 RefObject<T | null>을 반환하도록 특별히 오버로드를 제공했기 때문임
    • 핵심 포인트

      • 타입은 분명 RefObject<T>지만, React의 실제 구현체에서는 이 ref 객체는 여전히 mutable
      • 즉, 타입은 readonly처럼 보일 수 있어도 실제 런타임에서는 수정이 가능함
        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이 됨

0개의 댓글