제가 타입스크립트를 사용하면서 코딩을 하다보면 꼭 그냥 지나치는법이 없는 타입에러들이 있습니다. 고것은 바로 event, dom, useRef의 타입에러입니다. 이번 포스팅은 자주 사용되지는 않지만 꼭 발생했던 useRef 타입에러에 대해 공부해보고 에러없이 사용할 수 있도록 해보자.
useRef가 무엇인지 모르시면 useRef 훅은 따로 공부하시는게 좋을 것 같습니다. 간단하게만 설명하자면 hook 중에 하나로, 지정한 초깃값을 객체의 .current 프로퍼티에 저장하는 것. DOM객체의 값을 직접 접근하거나, 변경할 때 사용한다. 주로 인풋 포커싱, 불필요한 리랜더링을 방지하기 위해 사용한다.
그럼 본론으로 넘어가서 useRef 타입에러 해결에 대해서 알아보자.
useRef 에러 예시 코드
// Signup.tsx
import { useRef } from "react";
const Signup = () => {
const emailRef = useRef();
const nameRef = useRef();
const passwordRef = useRef();
return (
<div>
<input type="text" ref={emailRef} required />
<input type="text" ref={nameRef} required />
<input type="password" ref={passwordRef} required />
</div>
);
};
export default Signup;
위처럼 useRef를 통해 변수 3개(emailRef, nameRef, passwordRef)를 선언하고 각각 접근하려는 input element에 ref를 지정하여 각 변수에 input의 값을 저장하려고 하는 코드이다.
(alias)useRef<undefined>():React.MutableRefObject<undefined> (+2overloads),
The 'MutableRefObject<undefined>' type cannot be assigned to the 'LegacyRef<HTMLInputElement> | undefined' type. The 'MutableRefObject<undefined>' type cannot be assigned to the 'RefObject<HTMLInputElement>' type. The format of the 'current' property is incompatible. The 'undefined' type cannot be assigned to the 'HTMLInputElement | null' type.
하지만 위의 코드만 작성하면 ref에 위와 같은 에러가 발생합니다. 위의 에러내용을 해석해보면 아래와 같다. js에서는 위처럼만 작성해도 문제가 바로 드러나진 않았지만 ts에서는 하나의 useRef에 인자나 제네릭 타입에 따라 다른 것을 사용할 수 있도록 오버로드가 되었기 때문에 발생한 에러이다.
(alias)useRef<undefined>():React.MutableRefObject<undefined> (+2overloads),
'MutableRefObject<undefined>' 형식은 'LegacyRef<HTMLInputElement> | undefined' 형식에 할당할 수 없습니다. 'MutableRefObject<undefined>' 형식은 'RefObject<HTMLInputElement>' 형식에 할당할 수 없습니다.
'current' 속성의 형식이 호환되지 않습니다. 'undefined' 형식은 'HTMLInputElement | null' 형식에 할당할 수 없습니다.
그럼 타입정의를 어떻게하고, 반환타입은 어떻게 지정해주면 좋은지 알아보자.
useRef 타입정의
useRef는 3가지의 종류가 있다. 하나씩 살펴보자.
1. 인자의 타입과 제네릭의 타입이 T로 일치할 때, MutableRefObject<T>를 반환하는 경우
useRef<T>(initialValue: T): MutableRefObject<T>;
MutableRefObject<T>의 경우는, 많이 들어보았을 단어가 있다. mutable. 즉, current 프로퍼티를 직접 변경할 수 있다.
MutableRefObject는 단순히 <T> 제네릭으로 넘겨준 타입의 current 프로퍼티를 가진 객체이다.
2. 인자의 타입이 null을 허용할 때, RefObject<T>를 반환하는 경우
useRef<T>(initialValue: T | null): RefObject<T>;
초기값에 null이 될 수 있는 유니언 타입을 지정하여 RefObject를 반환한다.
RefObject는 1번의 MutableRefObject와는 달리 readonly 속성이기 때문에 값을 변화시킬 수 없는것이 차이점이다.
따라서 2번은 current 프로퍼티에 내용을 넣는 용도로는 사용할 수 없다.
const timeout = useRef(null); // RefObject를 반환하여 current가 readonly가 된다.
timeout.current = setTimeout(() => {
startTime.current = new Date().getTime();
}, Math.floor(Math.random() * 1000) + 3000);
예를 들어 위처럼 코드를 작성하면 에러가 발생한다. 왜일까?
위의 useRef는 RefObject를 반환하여 current가 readonly 이지만, 변경하려고 하기 때문에 에러가 발생하는 것이다.
const timeout = useRef<number | null>(null); // 해당 라인 수정
timeout.current = setTimeout(() => {
startTime.current = new Date().getTime();
}, Math.floor(Math.random() * 1000) + 3000);
위와 같이 코드를 수정해주면, 1번의 경우인 MutableRefObject를 반환하기 때문에 수정이 가능하여 에러를 발생시키지 않고 정상작동한다.
3. 제네릭에 undefined를 넣을 때, MutableRefObject<T | undefined>를 반환하는 경우
useRef<T = undefined>(): MutableRefObject<T | undefined>;
제네릭의 타입이 undefined이기 때문에 매개변수가 존재하지 않을 때 사용한다.
useRef 사용예시
사용예시를 2가지 용도로 나눌 수 있을 것 같다.
- 변경가능하고, 로컬변수로 활용될 수 있는 useRef를 어떠한 value를 증가 or 감소시키는 용도로 사용
- 읽기전용이고, element객체에 useRef를 통해 접근하기 위한 용도로 사용
첫번째 용도
첫번째 용도로 사용할 때의 코드 예시를 살펴보자.
첫번째의 사용예시에서 useRef의 타입정의는 어떻게하면 좋을까? 변경이 가능해야하고, 로컬변수로 활용될 수 있도록 useRef를 정의해보자. 타입정의의 1번 방법으로 타입을 정의하면 될 것 같다.
import { useRef } from "react";
const Example = () => {
const exampleRef = useRef<number>(0);
const btnClickHandler = () => {
if (exampleRef.current) {
exampleRef.current += 1;
}
};
return (
<div>
<button onClick={btnClickHandler}>PLUS</button>
</div>
);
};
export default Example;
위의 코드처럼 작성하게 되면, 타입정의 1번처럼 <>안에 들어있는 타입인 number와 초기값인 0의 타입이 같기 때문에 MutableRefObject가 반환된다. 따라서 current의 타입도 MutableRefObject의 타입인 number가 되어 정상적으로 작동한다.
즉, <>안의 타입과 초기값의 타입이 일치하면 current의 타입도 동일한 타입을 받게된다는 것이다.
만약 아래와 같은 코드가 있으면 <>안의 타입과 초기값의 타입이 string으로 일치하여 current의 타입도 string이 된다.
const exampleRef = useRef<string>("hello");
두번째 용도
두번째 용도로 사용할 때의 코드 예시를 살펴보자.
두번째의 사용예시에서 useRef의 타입정의는 어떻게 하면 좋을까? 읽기전용이고, element객체에 접근할 수 있도록 useRef를 정의해보자. 타입정의의 2번 방법으로 타입을 정의하면 될 것 같다.
import { useRef } from "react";
const Example = () => {
const exampleRef = useRef<HTMLInputElement>(null);
const btnClickHandler = () => {
if (exampleRef.current) {
exampleRef.current.value = "";
}
};
return (
<div>
<input type="text" ref={exampleRef} />
<button onClick={btnClickHandler}>내용 삭제</button>
</div>
);
};
export default Example;
위의 코드처럼 작성하게 되면, 타입정의 2번처럼 <>안에 input DOM element를 넣어주어 수정할 수 없는 readonly인 current프로퍼티 RefObject<T>를 반환받아 직접 element에 접근하여 input의 value를 빈 문자열로 수정한다.
근데 이상한 점이 있다! 수정할 수 없는데 수정을 한다...? 이게 어떻게 가능할까??
가능한 이유는 readonly는 current 프로퍼티만 적용되고, shallow(얕은 객체 복사)이기 때문에 current의 하위 프로퍼티인 value는 readonly와 관계 없이 수정이 가능한 것이다.
그리고 또 해봐야 하는 것이 있다. 타입정의 3번인 useRef의 매개변수를 undefined로 바꾸어보는 것!
import { useRef } from "react";
const Example = () => {
const exampleRef = useRef(); // 매개변수를 없애주었다.
const btnClickHandler = () => {
if (exampleRef.current) {
exampleRef.current.value = "";
}
};
return (
<div>
<input type="text" ref={exampleRef} />
<button onClick={btnClickHandler}>내용 삭제</button>
</div>
);
};
export default Example;
위와 같이 하면, input태그의 ref에서 에러가 발생한다. 에러의 내용은 아래와 같다.
쉽게 말하면, ref 프로퍼티는 RefObject를 받아야하는데 exampleRef가 MutableRefObject가 되기 때문에, 맞지 않는 타입의 값을 집어넣으려고 하기 때문에 생긴 에러이다. 따라서 useRef의 사용 용도에 맞춰서 <>의 타입과 초기값을 알맞게 사용해주어야한다.
나중을 위한 간단 요약
1. useRef를 수정가능하고, 로컬변수로 사용하려면 <>의 타입과 초기값의 타입이 같도록 한다.
const exampleRef = useRef<같은타입>(같은타입의 값);
2. useRef로 읽기전용, DOM을 직접 조작하려면 <>에 HTML element를 넣어주고, 초기값에 null을 넣어준다.
const exampleRef = useRef<조작하고자 하는 element>(null);
3. useRef는 useRef의 타입정의와 초기값의 타입에 따라 2종류의 객체를 반환한다.
- 요약 1번의 용도와 방법을 통해 반환하는 MutableRefObject 객체
- 요약 2번의 용도와 방법을 통해 반환하는 RefObject 객체
이렇게 useRef의 타입정의에 대해서 알아보았다. 이제부터는 useRef에서 타입에러를 겪지 않게 useRef의 용도에 맞게 타입정의를 잘해주도록 해보자!