[React, TS] useRef의 여러가지 타입

장동균·2022년 5월 12일
3

정의

useRef의 반환 타입에 대해서 먼저 알아본다.

interface MutableRefObject<T> {
  current: T;
}

interface RefObject<T> {
  readonly current: T | null;
}

두 종류의 반환 타입이 있으며 RefObject.current는 수정이 불가능하다는 것을 확인할 수 있다.


@types/react를 보면 useRef 훅은 3개의 정의가 오버로딩 되어 있는 것을 확인할 수 있습니다.

useRef<T>(initialValue: T): MutableRefObject<T>

제네릭 타입과 initialValue의 타입이 T로 일치하는 경우, MutableRefObject<T>를 반환한다. Mutable이라는 단어에서 확인할 수 있듯이 Ref.current 자체에 대한 수정이 가능하다.

useRef<T>(initialValue: T | null): RefObject<T>

initialValue의 타입이 null을 허용하는 경우, RefObject<T>를 반환한다. Ref.current 자체에 대한 수정이 불가능하다.

useRef<T = undefined>(): MutableRefObject<T | undefined>

제네릭 타입이 undefined인 경우(타입을 제공하지 않은 경우), MutableRefObject<T | undefined>를 반환한다.


예제

import { useEffect, useRef } from 'react'

const App = () => {
  const value = useRef<number>(0)  // MutableRefObject<number>
  
  useEffect(() => {
    value.current += 1
  }, [])
}

제네릭 타입과 initialValue의 타입이 number로 일치한다. 때문에 value의 타입은 MutableRefObject<number>가 된다. Mutable 하기 때문에 Ref.current에 대한 수정이 가능하다. 때문의 위의 코드는 정상적으로 동작한다.


import { useEffect, useRef } from 'react'

const App = () => {
  const value = useRef<number>(null)  // RefObject<number>
  
  useEffect(() => {
    value.current += 1  // 에러!
  }, [])
}

initialValuenull이 사용되면서 value의 타입은 RefObject<number>가 된다. RefObjectRef.current에 대한 수정이 불가능하다. 떄문에 위의 코드는 에러가 난다.


import { useEffect, useRef } from 'react'

const App = () => {
  const inputRef = useRef<HTMLInputElement>(null)  // RefObject<HTMLInputElement>
  
  useEffect(() => {
    inputRef.current?.value = "no error"
  }, [])
}

initialValuenull이 사용되면서 value의 타입은 RefObject<HTMLInputElement>가 된다. RefObjectRef.current에 대한 수정이 불가능하다. 하지만 위의 코드는 에러가 나지 않는다.

위에서 봐왔던 내용에 의하면 당연히 수정이 불가능해야 할 것 같은데 왜 이 경우에서는 에러가 발생하지 않는 것일까? 그 이유는 readonly로 선언된 current가 객체라는 점에 있다. readonlyshallow하게 동작하기 때문에 current의 하위 프로퍼티 value는 수정이 가능하다.


새로운 질문

그렇다면 useRef를 통해 만든 RefObject/MutableRefObjectprops로 내보내야 할 때 props의 타입 정의는 어떻게 할 수 있을까? (당연히 forwardRef를 사용하지 않는 경우입니다. ex) 전역함수를 호출하는데 인자로 ref를 받아야하는 경우)

이때 사용할 수 있는 타입은 Ref<T>, RefObject<T>, MutableRefObject<T> 이 3개가 있습니다.

이 세 개 타입을 구분하기 전에, 모던 리액트에서 ref는 두 종류가 있다는 것을 기억해야 합니다.

  1. ref 객체
  2. ref 콜백

ref 객체는 useRef(functional component) 혹은 createRef(class component)에 의해 만들어집니다. 이 객체는 current라는 프로퍼티를 가집니다.

ref 콜백은 거의 쓰이지 않는 경우입니다. current라는 프로퍼티를 가지지 않으며 (instance: T) => void의 타입을 가집니다.

Ref<T>ref 객체와 ref 콜백 모두를 가지는 타입입니다.

RefObject<T>, MutableRefObject<T>ref 객체만을 가지는 타입입니다. (이 둘의 차이는 Ref.current에 대한 수정 가능 여부)


예제

interface ExampleProps {
  buttonRef: Ref<HTMLButtonElement>
}

const Example: FC<ExampleProps> = ({ buttonRef }) => {
  return (
    <div>
      <button ref={buttonRef}>Hello</button>
    <div>
  )
}

위의 코드는 에러를 발생시키지 않습니다. buttonRef에 대해 그 어떠한 작업도 하지 않고 있기 때문입니다.

interface ExampleProps {
  buttonRef: Ref<HTMLButtonElement>
}

const Example: FC<ExampleProps> = ({ buttonRef }) => {
  useEffect(() => {
    console.log(buttonRef.current);
  });
  return (
    <div>
      <button ref={buttonRef}>Hello</button>
    <div>
  )
}

하지만 이 코드는 에러가 발생합니다. 그 이유는 buttonRef의 타입이 ref 객체일 수도, ref 콜백일 수도 있기 때문입니다. ref 객체에는 current 프로퍼티가 존재하지만, ref 콜백에서는 current 프로퍼티가 존재하지 않습니다. 이로 인해 ref 객체인지, ref 콜백인지 알 수 없는 buttonRefcurrent 프로퍼티를 조회할 수는 없습니다.

interface ExampleProps {
  buttonRef: RefObject<HTMLButtonElement>
}

const Example: FC<ExampleProps> = ({ buttonRef }) => {
  useEffect(() => {
    console.log(buttonRef.current);
  });
  return (
    <div>
      <button ref={buttonRef}>Hello</button>
    <div>
  )
}

Ref<T> 대신 RefObject<T> 혹은 MutableRefObject<T>를 사용하여 buttonRef의 타입을 ref 객체로 좁히고 current 프로퍼티를 조회하는 방식으로 코드를 수정해볼 수 있습니다.


LegacyRef

useRef의 타입에는 MutableRefObject, RefObject 뿐만 아니라 LegacyRef도 존재합니다.
LegacyRef 타입이 발생 되는 순간은 타입을 HTMLElement로 지정하면서 초기값을 undefined로 둘 때 입니다.

const node = useRef<HTMLElement>();  // node의 타입은 LegacyRef

참고링크


번외

ref callback은 다음 코드의 모양이라고 합니다.

interface ExampleProps {
  buttonRef: Ref<HTMLButtonElement>
}

const Example: FC<ExampleProps> = ({ buttonRef }) => {
  return (
    <div>
      <button ref={(element) => {
        if (typeof buttonRef === 'function') {
          buttonRef(element);
        } else {
          buttonRef.current = element;
        }
      }}>Hello</button>
    <div>
  )
}

이런 식으로 ref의 값을 콜백 함수로 받을 때가 ref callback에 해당합니다.


참고문헌

https://stackoverflow.com/questions/65876809/property-current-does-not-exist-on-type-instance-htmldivelement-null

https://driip.me/7126d5d5-1937-44a8-98ed-f9065a7c35b5

profile
프론트 개발자가 되고 싶어요

3개의 댓글

comment-user-thumbnail
2022년 5월 17일

ㄱ?

답글 달기
comment-user-thumbnail
2022년 5월 23일

ㄱ?

1개의 답글