[React] MutableRefObject와 LegacyRef

2022. 2. 4. 11:38Programming/React

반응형

InputonChangesetState를 할당해서 값이 바뀔때마다 상태값이 변경되도록 설정했더니, 매번 Input에 입력한 값이 변경될 때마다 렌더링이 다시 되는 기염을 토했다. 당연한 얘긴 줄 알고 있었는데 주리님이 보시고 ‘이럴때는 이런 식으로 최적화가 가능해요’라고 말씀해주셔서, react-hook-form, react-final-form, formic같은 애들에 대해 알게됐다. 조금 고민해보다가 ‘엥? 얘는 일단 간단하니까 useRef만 써도 되는거 아닐까?’라는 결론에 도달, Inputref를 냅다 꽂아버렸더니 LegacyRefMutableRefObject를 할당할 수 없다는 에러가 발생했다.

사건의 발단이 된 타레. 주리님 말대로 useRef에 초기값으로 null을 넣어보니 감쪽같이 Typescript 에러가 사라진다. 짜잔!

우선 Inputref를 보면 요 녀석은 LegacyRef|undefined를 받게 되어있다. 요 녀석을 따라가보자.

 

ref 자체가 LegacyRef나 undefined만 할당할 수 있는 녀석이었다.

Input이 어째서 LegacyRef를 참조하는지는 IntrinsicElement를 살펴보면 알 수 있다. IntrinsicElementDom elements에다, React 내에서 추가한 props를 처리할 수 있도록 만들어놓은 녀석인 듯(?)하다. 요 녀석부터 살펴보자.

interface IntrinsicElements {
  // HTML
  a: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
  abbr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
  address: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
  ...
    input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
    ...
type DetailedHTMLProps<E extends HTMLAttributes<T>, T> = ClassAttributes<T> & E;

interface ClassAttributes<T> extends Attributes {
  ref?: LegacyRef<T> | undefined;
}

interface RefObject<T> {
  readonly current: T | null;
}
type RefCallback<T> = { bivarianceHack(instance: T | null): void }["bivarianceHack"];
type Ref<T> = RefCallback<T> | RefObject<T> | null;
type LegacyRef<T> = string | Ref<T>;

IntrinsicElements에서 사용하고 있는 DetailedHTMLProps를 살펴보면, HTMLAttributesClassAttributesIntersection Types이고, ClassAttributes는 안에 LegacyRef만 가지고 있는 녀석이다. 정리해보면 DetailedHTMLPropsHTMLAttributesLegacyRef만 추가된 녀석이라고 볼 수 있다. 와! 차근차근 살펴보니까 생각보다 심플해! 여기서 DetailedHTMLPropsEReact.InputHTMLAttributes<HTMLInputElement>이고, THTMLInputElement로 고정되는 것을 알 수 있다.

아무튼 LegacyRefstring|Ref를 인자로 받을 수 있다. LegacyRefstring을 받을 수 있는 이유에 대해 살펴보자면, ReactClass components를 사용하던 시절로 거슬러 올라가야 한다. 그 당시에는 ref를 사용하기 위해선 키 값을 지정했기 때문에, 키 값에 해당하는 string을 인자로 받는 코드가 남아있는 것이다. 자, 그럼 이제 LegacyRef가 인자로 받을 수 있는 또 하나의 타입인 Ref를 살펴보자.

RefRefCallbackRefObject, 그리고 null을 받을 수 있다. 위에서 THTMLInputElement로 고정되었으니까, RefT 역시 HTMLInputElement가 된다.

이제 useRef를 살펴보자. useRef는 넘겨준 T에 따라 세 가지 방식으로 오버로딩되어있는데, useRef의 반환형 중 RefObject를 찾아볼 수 있다. 반환형이 RefObject가 아닌 MutableRefObject인 경우가 있는데, 살펴보면 RefObject와는 달리 current를 변경 가능할 뿐, 똑같이 생긴 녀석임을 알 수 있다.

function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T|null): RefObject<T>;
function useRef<T = undefined>(): MutableRefObject<T | undefined>;

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

interface MutableRefObject<T> {
  current: T;
}

여기서 다시 에러가 났던 코드로 돌아가자.

useRef가 반환하는 녀석을 보면 MutableRefObject<HTMLInputElement | undefined>를 반환하는 걸 볼 수 있다. 뭐, THTMLInputElement로 주고 초기값이 undefined니까 자연스럽게 THTMLInputElement|undefined로 추론되는 것.

문제는 refTIntrinsicElementsDetailedHTMLProps에 의해서 HTMLInputElement로 고정됐는데, useRef의 반환형은 초기값이 undefined이므로 THTMLInputElement | undefined로 고정된다. 결국 여기서 T가 다르기때문에, ref에 내려꽂으면 에러가 발생하는 것!

이제 useRef<HTMLInputElement>()useRef<HTMLInputElement>(null)로 변경하면 무슨 일이 일어나는지 살펴보자.

useRef의 반환형이 RefObject<HTMLInputElement>로 바뀌었다! 위에서 살펴봤던 세 가지 형태의 useRef에서, T값이 변경되어 반환형도 달라졌기 때문이다.

function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T|null): RefObject<T>;
function useRef<T = undefined>(): MutableRefObject<T | undefined>;

이제 할당하고자 하는 ref의 타입과 useRef의 반환형이 동일하므로, 에러가 발생하지 않게됐다. 와! 고민 해결!

반응형

'Programming > React' 카테고리의 다른 글

[React] Remix에 Pico.css를 적용해보자  (0) 2021.12.21