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 // 에러!
}, [])
}
initialValue
로 null
이 사용되면서 value
의 타입은 RefObject<number>
가 된다. RefObject
는 Ref.current
에 대한 수정이 불가능하다. 떄문에 위의 코드는 에러가 난다.
import { useEffect, useRef } from 'react'
const App = () => {
const inputRef = useRef<HTMLInputElement>(null) // RefObject<HTMLInputElement>
useEffect(() => {
inputRef.current?.value = "no error"
}, [])
}
initialValue
로 null
이 사용되면서 value
의 타입은 RefObject<HTMLInputElement>
가 된다. RefObject
는 Ref.current
에 대한 수정이 불가능하다. 하지만 위의 코드는 에러가 나지 않는다.
위에서 봐왔던 내용에 의하면 당연히 수정이 불가능해야 할 것 같은데 왜 이 경우에서는 에러가 발생하지 않는 것일까? 그 이유는 readonly
로 선언된 current
가 객체라는 점에 있다. readonly
는 shallow
하게 동작하기 때문에 current
의 하위 프로퍼티 value
는 수정이 가능하다.
그렇다면 useRef
를 통해 만든 RefObject/MutableRefObject
를 props
로 내보내야 할 때 props
의 타입 정의는 어떻게 할 수 있을까? (당연히 forwardRef
를 사용하지 않는 경우입니다. ex) 전역함수를 호출하는데 인자로 ref
를 받아야하는 경우)
이때 사용할 수 있는 타입은 Ref<T>
, RefObject<T>
, MutableRefObject<T>
이 3개가 있습니다.
이 세 개 타입을 구분하기 전에, 모던 리액트에서 ref
는 두 종류가 있다는 것을 기억해야 합니다.
ref
객체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
콜백인지 알 수 없는 buttonRef
가 current
프로퍼티를 조회할 수는 없습니다.
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
프로퍼티를 조회하는 방식으로 코드를 수정해볼 수 있습니다.
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
에 해당합니다.
ㄱ?