Ref & forwardRef & useImperativeHandle

숭이·2022년 2월 9일
1

React

목록 보기
1/2

Ref란?

Ref는 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법을 제공합니다. (React 공식 문서)

리액트에서 Ref를 사용하는 이유

HTML의 경우 id를 사용하여 DOM 요소에 직접 접근합니다.

ex. <div id="exampleID">

하지만 React의 경우 다음과 같은 이유로 위와 같은 방법을 지양하고 있습니다.

  • 이유 1 : React는 state로 조작되기 때문에 Dom API와 혼합해서 데이터 및 조작을 할 경우 디버깅이 어려워지고 유지보수가 어려운 코드가 된다.

  • 이유 2 : 하나의 컴포넌트가 여러번 생성되는 경우 같은 ID를 가지기 때문에 특정 DOM 객체를 querySelector, getElementById로 판별하기 어렵다.

Ref를 사용하는 경우

Ref를 사용하는 경우는 크게 3가지 경우입니다.

  1. React에서 state로만 해결할 수 없고 DOM을 반드시 직접 건드려야 할 경우

  2. 컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리하는 경우

  3. 클래스의 인스턴스 프로퍼티와 유사하게 사용하는 경우

DOM을 직접 건드려야 하는 경우

대부분의 경우 state를 사용하여 '어떤 일이 일어나게' 할 수 있습니다. 하지만 다음과 같은 경우 DOM에 직접 접근하는 방법이 필요합니다.

  1. 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때.
  2. 애니메이션을 직접적으로 실행시킬 때.
  3. 서드 파티 DOM 라이브러리를 React와 같이 사용할 때.

컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리하는 경우

useRef로 관리하는 변수는 순수 자바스크립트 객체를 생성하기 때문에 매번 렌더링을 할 때 동일한 ref 객체를 제공합니다.

따라서 리렌더링이 발생할 경우 함수 내 로컬 변수들은 초기화되지만 useRef로 관리하는 변수는 값이 초기화 되지 않고 마지막으로 업데이트한 current 값이 유지됩니다.

또한 Ref의 값이 변경되어도 리렌더링이 발생하지 않습니다.

React가 DOM 노드에 ref를 attach하거나 detach할 때 어떤 코드를 실행하고 싶다면 대신 콜백 ref를 사용하세요.

리액트 컴포넌트에서의 상태는 상태를 바꾸는 함수를 호출하고 나서 그 다음 렌더링 이후로 업데이트 된 상태를 조회 할 수 있는 반면, useRef로 관리하고 있는 변수는 설정 후 바로 조회 할 수 있습니다.

// 렌더링 이후로 업데이트 된 상태를 조회할 수 있는 useState()와 달리 설정 후 바로 조회할 수 있습니다.
// unmount 했을 시 +1씩 되고 있던 counter 의 값이 alert 됩니다.
function Counter() {
  const counter = useRef();
  useEffect(() => {
    counter.current = 0;
    const timer = setInterval(() => {
      counter.current += 1;
    }, 1000);
    return () => {
      clearInterval(timer);
      alert(counter.current);
    };
  }, []);
  return (
    <div>
      <p>{counter.current}</p>
    </div>
  );
}

클래스의 인스턴스 프로퍼티와 유사하게 사용하는 경우

useRef()는 DOM ref만을 위한 것이 아닙니다.

“ref” 객체는 현재 프로퍼티가 변경할 수 있고 어떤 값이든 보유할 수 있는 일반 컨테이너입니다.

이는 class의 인스턴스 프로퍼티와 유사합니다.

이벤트 처리에서 인터벌을 지우고 싶을 때 유용합니다.

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });
}

// ...
function handleCancelClick() {
  clearInterval(intervalRef.current);
}
// ...

Ref 접근

render 메서드 안에서 ref가 엘리먼트에게 전달되었을 때, 그 노드를 향한 참조는 ref의 current 어트리뷰트에 담기게 됩니다.

  1. ref가 HTML 엘리먼트에 쓰였다면, 생성된 ref는 자신을 전달받은 DOM 엘리먼트를 current 프로퍼티의 값으로서 받습니다.

  2. ref가 커스텀 클래스 컴포넌트에 쓰였다면, ref 객체는 마운트된 컴포넌트의 인스턴스를 current 프로퍼티의 값으로서 받습니다.

  3. 함수 컴포넌트는 인스턴스가 없기 때문에 함수 컴포넌트에 ref 어트리뷰트를 사용할 수 없습니다.

ref 생성 방법

useRef란?

useRef는 .current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환합니다. 반환된 객체는 컴포넌트의 전 생애주기를 통해 유지될 것입니다.(React 공식 문서)

createRef() Vs useRef()

class형 컴포넌트와 function형 컴포넌트는 리렌더링시 작동 방식의 차이가 있다.

  • class형 컴포넌트 : 인스턴스를 생성한 후에 render 코드 블록쪽만 리랜더링 후 다시 실행한다.
  • function형 컴포넌트 : 함수 블록안에 있는 모든 것을 리랜더링 시마다 다시 실행한다.

class형 컴포넌트에서 ref를 생성해야 하는 경우 createRef()를 사용한다.

function형 컴포넌트에서 ref를 생성해야 하는 경우 createRef()와 useRef()를 둘 다 사용할 수는 있다.

하지만 createRef()를 사용할 경우 리렌더링 될 때마다 ref 값이 초기화되어 원하는 값을 얻지 못하므로 ref 객체가 유지되는 useRef()를 사용한다.

callback Ref

클래스형 컴포넌트의 경우 React는 ref가 설정되고 해제되는 상황을 세세하게 다룰 수 있는 callback ref라 불리는 ref를 설정하기 위한 또다른 방법을 제공합니다.

만약 리액트 낮은 버전을 사용하고 있어서(react 16.3이전 버전) React.createRef()를 사용할 수 없다면, 콜백 ref를 사용하여 ref를 생성하게 됩니다.

콜백 ref를 사용할 때는 ref 어트리뷰트에 createRef() 를 통해 생성된 ref를 전달하는 대신 함수를 전달합니다.

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;

    this.setTextInputRef = element => {
      this.textInput = element;
    };

    this.focusTextInput = () => {
      // DOM API를 사용하여 text 타입의 input 엘리먼트를 포커스합니다.
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 마운트 되었을 때 자동으로 text 타입의 input 엘리먼트를 포커스합니다.
    this.focusTextInput();
  }

  render() {
    // text 타입의 input 엘리먼트의 참조를 인스턴스의 프로퍼티
    // (예를 들어`this.textInput`)에 저장하기 위해 `ref` 콜백을 사용합니다.
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

주의점

ref 콜백이 인라인 함수로 선언되어있다면 ref 콜백은 업데이트 과정중에 처음에는 null로 , 그다음에는 DOM 엘리먼트로 총 2번 호출됩니다.

매 랜더링마다 ref 콜백의 새 인스턴스가 생성되므로 React가 이전의 ref를 제거하고 새 ref를 설정해야 하기 때문에 일어납니다.

이러한 현상은 ref 콜백을 클래스에 바인딩된 메서드로 선언함으로써 해결할 수 있습니다.

forwardRef

forward Ref는 컴포넌트를 통해 자식 중 하나에 ref를 자동으로 전달하는 기법입니다.

재사용 가능한 컴포넌트 라이브러리와 같은 어떤 컴포넌트에서는 유용할 수 있습니다.

(애플리케이션 레벨의 컴포넌트에서는 바람직하지만, 어플리케이션 전체에 걸쳐 재사용되는 경향이 있는 “말단” 요소에서는 불편할 수도 있습니다. 또한 focus, 선택 등을 관리하기 위해서는 DOM 노드에 접근하는 것이 불가피 할 수 있습니다.)

React 컴포넌트를 forwardRef() 함수로 감싸주면 해당 컴포넌트는 함수는 두 번째 매개 변수를 갖게 되는데, 이를 통해 외부에서 ref prop을 넘길 수 있습니다.

// Player.jsx
import React, { useRef } from "react";
import Audio from "./Audio";
import Controls from "./Controls";

function Player() {
  const audioRef = useRef(null);

  return (
    <>
      <Audio ref={audioRef} />
      <Controls audio={audioRef} />
    </>
  );
}

export default Player;
// Audio.jsx
// forwardRef를 사용하여 두번째 매개변수로 ref객체에 audio 엘리먼트를 저장하여 넘겨줄 수 있습니다.
import React, { forwardRef } from "react";

function Audio(prop, ref) {
  return (
    <>
      <audio src={music} ref={ref}>
        audio
      </audio>
    </>
  );
}

export default forwardRef(Audio);
// Controls.jsx
// ref가 아닌 audio라는 일반적인 prop으로 audioRef 객체가 넘기기 때문에 굳이 forwardRef() 함수를 사용할 필요가 없습니다.
import React from "react";

function Controls({ audio }) {
// audioRef 객체를 통해서 audio 엘리먼트의 play()와 pause() 함수를 호출할 수 있습니다.
  const handlePlay = () => {
    audio.current.play();
  };

  const handlePause = () => {
    audio.current.pause();
  };

  return (
    <>
      <button onClick={handlePlay}>재생</button>
      <button onClick={handlePause}>중지</button>
    </>
  );
}

export default Controls;

useImperativeHandle

useImperativeHandle 이란?

useImperativeHandle은 ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값을 사용자화(customizes)합니다. (React 공식문서)

useImperativeHandle을 사용하는 이유

  1. ref가 클래스 컴포넌트에 사용되는 것과 유사하게, 내부 메소드를 외부에 보낼 수 있습니다.

  2. 부모에게 꼭 자식의 실제 reference를 보내지 않고 우리가 원하는 일종의 proxy reference를 보내는게 가능하므로 컴포넌트간 독립성을 보장할 수 있습니다.

function ParentComponent() {
// 부모는 inputRef.current로 접근하여 자식의 메소드에 접근할 수 있습니다.
  const inputRef = useRef();
  return (
    <>
          <FancyInput ref={inputRef} />
          <button onClick={() => inputRef.current.realllyFocus()}>포커스</button>
    </>
  )
}
function FancyInput = React.forwardRef((props, ref) {
  // 부모는 이 로직에 대해 모르고, 위로 끌어올리지 않고도 그냥 ref.current로 접근하여 사용만 하면 된다
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    reallyFocus: () => {
      inputRef.current.focus();
      console.log('Being focused!')
    }
  }));
  // ref는 input을 참조하고 있다. 
  return <input ref={inputRef} />
})

부모는 자식의 DOM에 직접적으로 접근을 하는 것이 아니라 useImperativeHandle로 전달된 자식 내부 메서드에만 접근이 가능해집니다.

1개의 댓글

comment-user-thumbnail
2023년 1월 27일

정말 유익한 글이네요!

답글 달기