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

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

Input이 어째서 LegacyRef를 참조하는지는 IntrinsicElement를 살펴보면 알 수 있다. IntrinsicElement는 Dom 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를 살펴보면, HTMLAttributes와 ClassAttributes의 Intersection Types이고, ClassAttributes는 안에 LegacyRef만 가지고 있는 녀석이다. 정리해보면 DetailedHTMLProps는 HTMLAttributes에 LegacyRef만 추가된 녀석이라고 볼 수 있다. 와! 차근차근 살펴보니까 생각보다 심플해! 여기서 DetailedHTMLProps의 E는 React.InputHTMLAttributes<HTMLInputElement>이고, T는 HTMLInputElement로 고정되는 것을 알 수 있다.
아무튼 LegacyRef는 string|Ref를 인자로 받을 수 있다. LegacyRef가 string을 받을 수 있는 이유에 대해 살펴보자면, React가 Class components를 사용하던 시절로 거슬러 올라가야 한다. 그 당시에는 ref를 사용하기 위해선 키 값을 지정했기 때문에, 키 값에 해당하는 string을 인자로 받는 코드가 남아있는 것이다. 자, 그럼 이제 LegacyRef가 인자로 받을 수 있는 또 하나의 타입인 Ref를 살펴보자.
Ref는 RefCallback과 RefObject, 그리고 null을 받을 수 있다. 위에서 T는 HTMLInputElement로 고정되었으니까, Ref의 T 역시 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>를 반환하는 걸 볼 수 있다. 뭐, T를 HTMLInputElement로 주고 초기값이 undefined니까 자연스럽게 T가 HTMLInputElement|undefined로 추론되는 것.
문제는 ref의 T가 IntrinsicElements의 DetailedHTMLProps에 의해서 HTMLInputElement로 고정됐는데, useRef의 반환형은 초기값이 undefined이므로 T가 HTMLInputElement | 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 |
|---|