[TypeScript & React] #4 useRef 사용법

1Hoit·2023년 6월 27일

TypeScript & React

목록 보기
5/5
post-thumbnail

시작하며

타입스크립트를 사용하여 리액트 프로젝트를 만들던 중
Type 'MutableRefObject<... | undefined>' is not assignable to type ...
에러가 발생하였다.

useRef?

useRef는 React Hook의 일종으로, 인자로 넘어온 초깃값을 useRef 객체의 .current 프로퍼티에 저장한다.

즉, useRef는 current에 어떠한 값을 담고있는 상자를 만드는 React Hook이다.

const exRef = useRef(1);

위 코드에서 exRef는 그저 1의 값을 담고있는 상자에 불과하다.
우린 current를 통해서 useRef에 있는 값을 사용할 수 있게 된다.

const num = exRef.current; // num = 1

useRef 사용시기

  1. DOM 취득 용도

    DOM 객체를 직접 가리켜서 내부 값을 변경하거나 focus() 메소드를 사용하거나 하는 때에 주로 사용

  • 제네릭 : 참조하는 HTML 엘리먼트를 넣어준다.
  • 초기값: null을 넣어준다.
// DOM button 참조: 제네릭으로 html엘리먼트(button) 설정. 초기값은 null로 설정
const buttonRef = useRef<HTMLButtonElement>(null);

// DOM input 참조 : 제네릭으로 html엘리먼트(input) 설정
const inputRef = useRef<HTMLInputElement>(null);

주의: useRef로 취득하는 DOM은 최초 mount되기 전엔 null이다.

import React, { useEffect, useRef } from 'react';
import './App.css';

function App() {
  // useRef 사용
  const inputRef = useRef(null);
  
  // 최초 실행시 input에 focus 주기: 실패한다.
  inputRef.current.focus();
   
  console.log(inputRef.current); // null 이 찍힌다.

  return (
    <div className="App">
      {/* input 을 useRef로 참조 */}
      <input ref={inputRef}></input>
    </div>
  );
}

export default App;

주로 최초 렌더링 시,
컴포넌트 함수 구현부에서 useRef로 DOM을 취득하고,
바로 ref.current 로 참조하여 사용할 때 null이 되는 경우가 많다.

이유는
아직 return 부분이 실행되지 않아서, 실제 DOM에 반영되지 않았는데 참조하려 하기 때문이다.

즉, 컴포넌트함수의 return 부분이 호출되어 실제 DOM에 반영되어야만 DOM 취득이 가능하다.

useRef : 안전한 사용 방법

  1. didMount 되어 DOM에 반영된 이후 사용하자.
  • 최초에는 didMount 직후 호출되는 useEffect 에서 사용하는게 안전하다.

  • 이후 업데이트 재렌더링시에는 이미 DOM에 반영된걸 참조하므로 어디서나 사용 가능하다.

  1. 항상 유효성 체크후 사용하자.
  • 조건부 렌더링 등에 의해, 컴포넌트가 Unmount 되는 경우에도 null이 될 수 있다.

  • 따라서 if 문이나 단축평가를 이용해 유효성 검사 후에 사용하는게 안전하다.

import React, { useRef, useEffect } from "react";

export default function App() {
  // 최초 렌더링 시, DOM이 생성되지 않아 null 이다.
  const inputRef = useRef();
  
  // 실제 DOM에 반영된 이후, DOM취득 가능
  useEffect(() => {
    // ref는 항상 존재여부를 검사하고 사용해야 한다(단축평가 Good!)
    inputRef.current && inputRef.current.focus();
  });

  return (
    <div>
      <input ref={inputRef} type="text"></input>
    </div>
  );
}
  1. 값 저장 용도

    변경되어도 컴포넌트가 리렌더링되지 않도록 하기 위한 값들을 저장하기 위해서도 사용

    • 이는 useRef가 내용이 변경되어도 이를 알려주지 않기 때문이다.
    • .current 프로피터를 변경시키는 것은 리렌더링을 발생시키지 않고, 따라서 로컬 변수 용도로 사용할 수 있다.
  • 제네릭 : 값의 타입을 넣어준다.
  • 초기값 : 반드시 타입에 맞는 초기값을 할당해준다.
// 값 저장 용도(number)
const count = useRef<number>(0);
const text = useRef<string>("")

useRef 반환 타입(ref객체 타입) 2가지

useRef로 생성한 ref객체의 타입은 아래의 2가지로 정의된다.

1. DOM 취득 용도

ref 객체는 React.RefObject<제네릭> 타입이 되며, ref.current(참조하는 DOM) 값 자체는 수정이 불가하다

  • 단, ref.current.속성(DOM객체.속성)은 수정 가능하다.
React.RefObject<HTMLButtonElement>
React.RefObject<HTMLInputElement>

// current 프로퍼티 그 자체를 직접 변경이 불가능
interface RefObject<T> {
    readonly current: T | null;
}
  • 참고
const inputRef = useRef<HTMLInputElement | null>(null);

제네릭에 null을 명시한 경우,
ref객체는 MutableRefObject<HTMLInputElement | null> 타입으로 정의된다.

2. 값 저장 용도

ref 객체는 React.MutabeRefObject<제네릭> 타입이 되며, ref객체.current 의 값 수정이 가능하다.

//2 current 프로퍼티 그 자체를 직접 변경이 가능
interface MutableRefObject<T> {
    current: T;
}
  • 함수 초깃값을 .current에 저장한다.

만약 타입을 모르겠다면?

에디터에서 마우스 오버하면, 어떤 타입인지 바로 확인 가능하다.


useRef의 정의 3가지

@types/react의 index.d.ts를 보면 useRef 훅은 3개의 정의가 오버로딩되어있는 것을 확인할 수 있다
@types/react 공식문서

언제 어떤 useRef가 쓰이는지 몰라서 ref를 typing할 때 많은 에러들이 발생하는데, 세 개의 오버로딩된 useRef를 보자.

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

인자의 타입과 제네릭의 타입이 T로 일치하는 경우, MutableRefObject<T>를 반환한다.

  • current 프로퍼티 그 자체를 직접 변경할 수 있다.

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

인자의 타입이 null을 허용하는 경우, RefObject를 반환한다.

  • current 프로퍼티를 직접 수정할 수 없다.

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

제네릭의 타입이 undefined인 경우(타입을 제공하지 않은 경우), MutableRefObject<T | undefined>를 반환한다.


예제

1번의 경우 : 값 저장 용도, current 수정가능

import React, { useRef } from "react";

const App = () => {
  //useRef<T>(initialValue: T): MutableRefObject<T>;
  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;

.current를 직접 수정할 수 있다.
useRef에 제네릭 타입과 동일한 타입의 초기 인자를 줬으므로, 여기에 사용된 useRef는 1번 케이스이다.
즉 localVarRef는 MutableRefObject<number> 타입이고, 그러므로 .current를 직접 수정하여 로컬 변수처럼 사용할 수 있는 것이다.


위의 코드를 2번의 경우로 변경해보자 useRef에 인자를 null로 초기화
2번 경우 : current 프로퍼티를 수정불가

  ...
  //위의 코드를 이처럼 변경
  const localVarRef = useRef<number>(null);

  const handleButtonClick = () => {
    localVarRef.current += 1; //에러 발생
    console.log(localVarRef.current);
  };
...

제네릭 타입은 number인데, 인자값에 null이 허용되었기 때문에
current 프로퍼티를 수정할 수 없는 것을 볼 수 있다.
이는 여기에서 사용된 useRef가 2번 경우로,
.current가 readonly인 RefObject를 반환했기 때문이다.
이 때는 current를 직접 변경할 수 없다.
따라서 에러가 발생한다.


다른 예시를 한번 보자.
input DOM element를 ref로 받고 null 로 초기화한다.

import React, { useRef } from "react";

const App = () => {
//위 코드와 다른 점은 input DOM element를 ref로 받음
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;

위 코드는 2번의 경우이다.
하지만 이번에는 current를 직접 건드리는게 아닌 current.value를 수정하고 있다.
이 것은 가능하다.
정의 상 current 프로퍼티만 읽기 전용으로, current 프로퍼티의 하위 프로퍼티인 value는 수정 가능하다.
이는 readonly가 shallow하기 때문이다.
단, 여전히 current를 직접 변경하는 것은 안 된다.


여기에서 다음과 같이 useRef의 인자를 undefined로 바꿔보자.

...
const inputRef = useRef<HTMLInputElement>();
...

이렇게 하면, <input ref={inputRef} /> 쪽에서 에러가 발생한다.
ref 프로퍼티는 RefObject형만 받는데, inputRef는 정의 상 MutableRefObject가 되고, 이를 ref 프로퍼티에 집어넣으려 해서 발생하는 에러이다.


결론

그럼 어떻게 useRef를 사용해야 할까?

로컬 변수 용도로 useRef를 사용하는 경우

MutableRefObject<T>를 사용해야 하므로 제네릭 타입과 같은 타입의 초깃값을 넣어주자.

const localVarRef = useRef<number>(0);

DOM을 직접 조작하기 위해 프로퍼티로 useRef 객체를 사용할 경우

RefObject<T>를 사용해야 하므로 초깃값으로 null을 넣어주자.

const inputRef = useRef<HTMLInputElement>(null);
profile
프론트엔드 개발자를 꿈꾸는 원호잇!

0개의 댓글