useRef

2023-09-17
ReactHooks

Table of Contents

useRef가 나오게 된 배경

  1. React에선 DOM에 직접 수정하는 것을 지양합니다. (document.querySelector 등..) 가상 DOM을 사용하기 때문입니다.
    • 가상 DOM이 아닌 실제 DOM을 제어하게 된다면, React의 구조에서 벗어나게 되고, 예상치 못한 side Effect를 유발합니다.
  2. useState에 DOM 상태 값을 관리하게 된다면 불필요한 많은 리렌더링이 발생하게 됩니다.
  3. DOM의 상태를 저장하는데, re-rendering이 안되게 해야 할 때 쓰는 Hook. 여기서 비제어, 제어 컴포넌트의 개념이 나오게 됩니다.

제어, 비제어 컴포넌트

  • 제어 컴포넌트는 사용자가 입력한 값과 저장되는 값이 실시간으로 동기화됩니다(useState). 반대로 비제어 컴포넌트는 사용자가 입력한 값을 실시간으로 동기화하지 못합니다. 리액트에서 감지하지 못하기 때문이죠.
    • 왜 감지하지 못하나요?
      1. useRef() 는 heap영역에 저장되는 일반적인 자바스크립트 객체입니다.
      2. 매번 렌더링할 때 동일한 객체를 제공한다. heap에 저장되어 있기 때문에 애플리케이션이 종료되거나 GC될 때 까지, 참조할때마다 같은 메모리 값을 가진다고 할 수 있습니다.
      3. 값이 변경되어도 리렌더링이 되지 않습니다. 같은 메모리 주소를 갖고있기 때문에 자바스크립트의 연산이 항상 true 를 반환합니다. 즉 변경사항을 감지할 수 없어서 리렌더링을 하지 않는다는 뜻입니다.

1. 프로젝트 세팅 React + TS + Vite

$ npm init vite@latest React-UseRef -- --template react-ts
$ cd React-UseRef
$ npm i

2. useRef 사용해보기

1. 저장공간으로 활용

/src/Ref.tsx

import { useRef, useState } from 'react';

const RefCounter = () => {
  // rendering trigger
  const [, setRender] = useState(false);
  // ref
  const ref = useRef<number>(0);
  // 일반 변수
  let val = 0;

  // ref 및 변수 change
  function valueChange() {
    // 상태값 저장도 할 수 있다.
    console.log('handle click useRef ::: ' + ref.current);
    ref.current = ref.current + 1;
    console.log('handle click 일반변수 ::: val ' + val);
    // val에 1더하기
    val++;
    alert('일반 변수 val = ' + val + ' useRef.current = ' + ref.current);
  }

  console.log('Ref Component Update', val, ref.current);

  // 강제 렌더링
  function render() {
    setRender((prev) => !prev);
  }

  return (
    <>
      <button onClick={valueChange}>Click Changed Ref Value!</button>
      <button onClick={render}>Force Rendering!</button>
    </>
  );
};

export default RefCounter;
  • 인자로 넘어온 초깃값을 useRef 객체의 .current 프로퍼티에 저장합니다.

/src/App.tsx

import "./App.css";
import RefCounter from "./Ref";

function App() {
  return (
    <>
      <RefCounter />
    </>
  );
}

export default App;
  • ref의 값이 나오게되고, 값은 일반변수도 같이 증가합니다.
  • useState와 다르게 렌더링을 하지않아 함수가 다시 실행되지 않고, 일반변수의 값이 초기화되지 않아 저장됩니다.

2. DOM 연결하기

/src/App.tsx

import { useRef, useState } from "react";
import "./App.css";

function App() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [state, setState] = useState("");

  // input 체인지 이벤트 발생 시 실행할 함수
  function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) {
    event.preventDefault();
    setState(event.target.value);
  }

  console.log("DOM UPDATE!");
  return (
    <>
      <h4>제어 컴포넌트</h4>
      <input type='text' onChange={onChangeHandler} />
      <div>useState state 값 ::: {state}</div>
      <hr />
      <h4>비제어 컴포넌트</h4>
      <div>Ref는 렌더링이 되지 않으면 확인할 수 없습니다.</div>
      <input type='text' ref={inputRef} />
      <div>useRef current.value 값 ::: {inputRef.current?.value}</div>
    </>
  );
}

export default App;
  • 제어 컴포넌트와 비제어 컴포넌트는 React 입장에서 제어할 수 있냐, 없냐라는 뜻입니다. Ref를 사용하게 된다면 React의 제어에서 벗어나게 됩니다.
  • 인자로 넘어온 초깃값을 useRef 객체의 .current 프로퍼티에 저장합니다. 이 방식에서는 useRef의 current 객체에 input값이 담겨 있겠죠.
  • useState가 리렌더링을 발생시키면 useRef에 할당된 값이 나오게됩니다. 값은 저장하지만, 렌더링은 발생시키지 않습니다.

3. Update되는 부모 컴포넌트에서 Ref의 동작

/src/Ref.tsx

import { useRef,
// useState
} from 'react';

const RefCounter = () => {
  // rendering trigger
  // const [, setRender] = useState(false);
  // ref
  const ref = useRef<number>(0);
  // 일반 변수
  let val = 0;

  // ref 및 변수 change
  function valueChange() {
    // 상태값 저장도 할 수 있다.
    console.log('handle click useRef ::: ' + ref.current);
    ref.current = ref.current + 1;
    console.log('handle click 일반변수 ::: val ' + val);
    // val에 1더하기
    val++;
    alert('일반 변수 val = ' + val + ' useRef.current = ' + ref.current);
  }

  console.log('Ref Component Update', val, ref.current);

  // 강제 렌더링
  // function render() {
  //   setRender((prev) => !prev);
  // }

  return (
    <>
      <button onClick={valueChange}>Click Changed Ref Value!</button>
      {/* <button onClick={render}>Force Rendering!</button> */}
    </>
  );
};

export default RefCounter;
  • 렌더링이 트리거 되는 부분은 주석처리하겠습니다.

/src/App.tsx

import { useRef, useState } from "react";
import "./App.css";
import RefCounter from "./Ref";

function App() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [state, setState] = useState("");

  // input 체인지 이벤트 발생 시 실행할 함수
  function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) {
    event.preventDefault();
    setState(event.target.value);
  }
  console.log("DOM UPDATE!");
  return (
    <>
      <h4>제어 컴포넌트</h4>
      <input type='text' onChange={onChangeHandler} />
      <div>useState state 값 ::: {state}</div>
      <hr />
      <h4>비제어 컴포넌트</h4>
      <div>Ref는 렌더링이 되지 않으면 확인할 수 없습니다.</div>
      <input type='text' ref={inputRef} />
      <div>inputRef state 값 ::: {inputRef.current?.value}</div>


      <RefCounter />
    </>
  );
}

export default App;
  • useState는 컴포넌트를 Updating 시킵니다. 그 과정에서 자식 컴포넌트들도 re-rendering 과정을 거치게 되는데요.
  • 자식 컴포넌트의 일반변수 val의 값은 초기화되고, Ref의 값은 저장되어 있습니다. 부모 컴포넌트가 Updating 되어도 자식 컴포넌트의 Ref값은 re-rendering이 되어도 값을 저장할 수 있다는 것을 확인할 수 있습니다.

3. 어떻게 써야할까?

Ref를 써야 할 경우

DOM 노드, 엘리먼트, React 컴포넌트 주소값을 활용하는 경우

  • <input> - focus()
  • media playback - play()pause()remove()
  • text selection
  • 애니메이션 적용
  • d3.js, greensock 등 DOM 기반 라이브러리 활용

State를 써야 할 경우

유저 데이터 실시간으로 받아와야 하는 경우

  • 유효성 검사
  • 조건문으로 버튼의 활성화 여부를 판단할 때

TypeScript에서의 useRef 타입에 따른 동작 방식

1. 값의 저장방식으로 사용

const localValRef = useRef<number>(0);
  • useRef는 T타입을 제네릭으로 할당하고, 초기값은 T타입이며, MutableRefObject에 T타입을 할당하여 리턴합니다.

    • MutableRefObject는 T타입의 current 프로퍼티를 반환합니다.

2. DOM에 연결방식으로 사용

const inputRef = useRef<HTMLInputElement>(null);
  • useRef가 T타입 혹은 null 타입을 받을때 RefObject에 T타입을 할당하여 리턴합니다.
  • RefObject는 readonly 속성이 들어간 T 혹은 null 타입 current 프로퍼티를 반환하는데, 실제 React의 DOM을 받아봤을때, 읽기 전용 속성인데 어떻게 수정을 할 수 있지? 라고 조금 헷갈릴 수 있습니다. React DOM을 할당한 Ref객체는 current객체를 수정하는 것이 아닌, current.value를 수정하기 때문에 readonly 속성을 벗어날 수 있습니다.
const localVarRef = useRef<number>(null);
  • 이 경우가 null이 할당된 경우입니다. 읽기 전용 속성은 수정할 수 없으므로 수정 로직 작성 시 타입에러를 리턴합니다.

인용 및 유용한 글들

https://react.dev/reference/react/useRef#examples-dom

https://velog.io/@xmun74/useRef

https://react.vlpt.us/basic/10-useRef.html

https://velog.io/@yukyung/React-제어-컴포넌트와-비제어-컴포넌트의-차이점-톺아보기