3️⃣

TypeScript React에서 useRef의 3가지 정의와 각각의 적절한 사용법

Date
2021/04/04
Tags
TypeScript
React
Created by
Type 'MutableRefObject<... | undefined>' is not assignable to type ... 에러 좀 그만 보자!
Table of Contents

useRef?

useRef는 React Hook의 일종으로, 인자로 넘어온 초깃값을 useRef 객체의 .current 프로퍼티에 저장한다. DOM 객체를 직접 가리켜서 내부 값을 변경하거나 focus() 메소드를 사용하거나 하는 때에 주로 사용하고, 변경되어도 컴포넌트가 리렌더링되지 않도록 하기 위한 값들을 저장하기 위해서도 사용한다. (이는 useRef가 내용이 변경되어도 이를 알려주지 않기 때문이다. .current 프로피터를 변경시키는 것은 리렌더링을 발생시키지 않고, 따라서 로컬 변수 용도로 사용할 수 있다.)
본질적으로 useRef는 .current 프로퍼티에 변경 가능한 값을 담고 있는 “상자”와 같습니다.
위의 말은 useRef의 반환 타입인 MutableRefObjectRefObject의 정의를 보면 더욱 명확하게 이해할 수 있다.
interface MutableRefObject<T> { current: T; } interface RefObject<T> { readonly current: T | null; }
TypeScript
복사
그저 함수 초깃값을 .current에 저장할 뿐이다.

useRef의 정의는 3개

@types/react의 index.d.ts를 보면 useRef 훅은 3개의 정의가 오버로딩되어있는 것을 확인할 수 있다 (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L1021-L1065). 언제 어떤 useRef가 쓰이는지 몰라서 ref를 typing할 때 많은 에러들이 발생하는데, 세 개의 오버로딩된 useRef를 하나씩 살펴보자.

1. useRef<T>(initialValue: T): MutableRefObject<T>;

인자의 타입과 제네릭의 타입이 T로 일치하는 경우, MutableRefObject<T>를 반환한다.
MutableRefObject<T>의 경우, 이름에서도 볼 수 있고 위의 정의에서도 확인할 수 있듯 current 프로퍼티 그 자체를 직접 변경할 수 있다.

2. useRef<T>(initialValue: T|null): RefObject<T>;

인자의 타입이 null을 허용하는 경우, RefObject<T>를 반환한다.
RefObject<T>는 위에서 보았듯 current 프로퍼티를 직접 수정할 수 없다.

3. useRef<T = undefined>(): MutableRefObject<T | undefined>;

제네릭의 타입이 undefined인 경우(타입을 제공하지 않은 경우), MutableRefObject<T | undefined>를 반환한다.
정의만 보아서는 제대로 이해하지 못할 수 있다. 예제를 보면서 이해해보자.

코드로 직접 보자

예제 몇 개를 살펴보자.
import React, { useRef } from "react"; const App = () => { const localVarRef = useRef<number>(0); const handleButtonClick = () => { if (localVarRef.current) { localVarRef.current += 1; console.log(localVarRef.current); } }; return ( <div className="App"> <button onClick={handleButtonClick}>+1</button> </div> ); }; export default App;
TypeScript
복사
간단하게 useRef를 로컬 변수 용도로 사용하는 경우이다. 버튼을 클릭할 경우 localVarRef.current의 값이 1씩 증가한다.
.current를 직접 수정할 수 있는 이유는 무엇일까? useRef에 제네릭 타입과 동일한 타입의 초기 인자를 줬으므로, 여기에 사용된 useRef는 1번 케이스이다. 즉 localVarRefMutableRefObject<number> 타입이고, 그러므로 .current를 직접 수정하여 로컬 변수처럼 사용할 수 있는 것이다.
만약 다음과 같이 useRef에 인자를 null로 초기화했다면 어떤 일이 벌어질까?
... const localVarRef = useRef<number>(null); const handleButtonClick = () => { localVarRef.current += 1; console.log(localVarRef.current); }; ...
TypeScript
복사
current 프로퍼티를 수정할 수 없는 것을 볼 수 있다. 이는 여기에서 사용된 useRef가 2번 경우로, .currentreadonlyRefObject를 반환했기 때문이다.
다른 예시를 한번 보자.
import React, { useRef } from "react"; const App = () => { const inputRef = useRef<HTMLInputElement>(null); const handleButtonClick = () => { if (inputRef.current) { inputRef.current.value = ""; } }; return ( <div className="App"> <button onClick={handleButtonClick}>+1</button> <input ref={inputRef} /> <button onClick={handleButtonClick}>Clear</button> </div> ); }; export default App;
TypeScript
복사
input DOM element를 ref로 받아서, 버튼을 클릭하면 input의 value를 직접 빈 문자열로 수정하는 예제이다. 이 예제에서 useRef는 2번 경우로, 정상적으로 동작한다.
잠깐! 2번 경우의 useRef는 수정 불가능한 RefObject<T>를 반환하는데, 왜 inputRef.current.value는 수정 가능한가요? → 정의 상 current 프로퍼티만 읽기 전용으로, current 프로퍼티의 하위 프로퍼티인 value는 여전히 수정 가능하다. 이는 readonly가 shallow하기 때문이다. HTMLInputElement를 받아온 시점에서 그럴 일은 없겠지만, current 프로퍼티를 직접 수정하려 하면 에러가 발생하는 것을 볼 수 있다.
여기에서 다음과 같이 useRef의 인자를 undefined로 바꿔보자.
... const inputRef = useRef<HTMLInputElement>(); ...
TypeScript
복사
이렇게 하면, <input ref={inputRef} /> 쪽에서 에러가 발생한다. ref 프로퍼티는 RefObject형만 받는데, inputRef는 정의 상 MutableRefObject가 되고, 이를 ref 프로퍼티에 집어넣으려 해서 발생하는 에러이다.

정리

정리하면 다음과 같다.
const localVarRef = useRef<number>(0);
TypeScript
복사
로컬 변수 용도로 useRef를 사용하는 경우, MutableRefObject<T>를 사용해야 하므로 제네릭 타입과 같은 타입의 초깃값을 넣어주자.
const inputRef = useRef<HTMLInputElement>(null);
TypeScript
복사
DOM을 직접 조작하기 위해 프로퍼티로 useRef 객체를 사용할 경우, RefObject<T>를 사용해야 하므로 초깃값으로 null을 넣어주자.

References