ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ref & useRef() & forwardRef()
    리액트 2021. 9. 11. 20:16

    공부하기 위해 리액트 공식 문서 및 리액트 블로그를 정리한 내용입니다.

    Ref

    일반적으로 리액트에서 DOM의 어느 자식을 수정하기 위해서는 새로운 props를 전달하여 자식을 다시 렌더링해야합니다. 그러나 이런 일반적인 방법을 벗어나 직접적으로 자식을 수정해야 하는 경우도 있습니다. 그 중 하나의 방법으로 Ref를 사용하실 수가 있습니다.

     

    Ref를 사용해야할 때

    리액트는 다음과 같은 사례에 Ref를 사용할 것을 권장합니다

    • focus, text selection, 미디어 재생 을 관리해야할 경우
    • 애니메이션을 직접 실행해야하는 경우
    • 서드 파티 DOM 라이브러리를 리액트와 같이 사용할 경우

    Ref 생성하기

    class 컴포넌트

    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.myRef = React.createRef(); // 중요
      }
      render() {
        return <div ref={this.myRef} />; // 중요
      }
    }

    Ref는 React.createRef()를 통해 생성하고 ref 속성을 통해 React 엘리먼트에 부착을 합니다. 

     

    Ref에 접근하기

    const node = this.myRef.current;

    위 코드의 render 메서드 중에 ref={this.myRef} 부분을 보겠습니다. 해당 코드를 통해 노드를 향한 참조는 ref.current 어트리뷰트에 담기게 됩니다. 즉 this.myRef.current의 값은 ref={this.myRef} 코드가 적힌 엘리먼트입니다.

    <div ref={this.myRef} /> 엘리먼트를 제어하고 싶다면 this.myRef.current를 통해 제어를 하실 수가 있습니다.

     

    클래스 컴포넌트와 Ref 사용 예

    class CustomTextInput extends React.Component {
      constructor(props) {
        super(props);
        // textInput DOM 엘리먼트를 저장하기 위한 ref를 생성합니다.
        this.textInput = React.createRef();
        this.focusTextInput = this.focusTextInput.bind(this);
      }
    
      focusTextInput() {
        // DOM API를 사용하여 명시적으로 text 타입의 input 엘리먼트를 포커스합니다.
        // 주의: 우리는 지금 DOM 노드를 얻기 위해 "current" 프로퍼티에 접근하고 있습니다.
        this.textInput.current.focus();
      }
    
      render() {
        // React에게 우리가 text 타입의 input 엘리먼트를
        // 우리가 생성자에서 생성한 `textInput` ref와 연결하고 싶다고 이야기합니다.
        return (
          <div>
            <input
              type="text"
              ref={this.textInput} />
            <input
              type="button"
              value="Focus the text input"
              onClick={this.focusTextInput}
            />
          </div>
        );
      }
    }

    위 코드는 input[type=button]을 클릭하면 input[type=text]에 focus가 되도록 하고자 합니다. input[type=button]을 클릭하면 focusTextInput 함수를 실행시킵니다. focusTextInput 함수는 ref를 통해 참조한 input[type=text]를 this.textInput.current를 통해  참조한다는 것을 아실 수가 있습니다.

     

    좀 더 구체적으로 말하자면 컴포넌트가 마운트될 때 React가 this.textInput.current 에 DOM 엘리먼트를 대입하는 것입니다. 그리고 컴포넌트의 마운트가 해제될 때 this.textInput.current의 값을 null로 돌려 놓습니다. 

     

    함수 컴포넌트

     

    함수 컴포넌트에서 ref를 사용하기 위해서는 useRef() 훅을 사용해야합니다. 

    const refContainer = useRef(initialValue);

    useRef는 .current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환합니다.

    즉 refContainer.current의 값은 initialValue인 상태로 초기화 됩니다.

     

    함수 컴포넌트와 useRef() 사용 예

    function TextInputWithFocusButton() {
      const inputEl = useRef(null);
      const onButtonClick = () => {
        // `current` points to the mounted text input element
        inputEl.current.focus();
      };
      return (
        <>
          <input ref={inputEl} type="text" />
          <button onClick={onButtonClick}>Focus the input</button>
        </>
      );
    }

    ref={inputEl}을 통해 리액트는 변경될때마다 변경된 DOM노드를 .current 프로퍼티를 설정합니다.

     

    useRef() 특징

     

    useRef()를 DOM의 특정 노드를 참조하는 것 외에도 다양하게 사용할 수 있습니다. useRef()를 통해 {current : ...} 객체를 생성하는데, 매번 렌더링할 때마다 동일한 ref 객체를 제공하기 때문에 어떤 가변값을 유지하는 데에도 사용할 수가 있습니다.  

     

    가변값 유지를 위한 useRef() 예시

     

    import React, { useEffect, useState } from "react";
    
    const App = () => {
      const [name, setName] = useState("");
      const [renderCount, setRenderCount] = useState(0);
    
      useEffect(() => {
        setRenderCount((prevRenderCount) => prevRenderCount + 1);
      });
    
      return (
        <>
          <input value={name} onChange={(e) => setName(e.target.value)} />
          <div>My name is {name}</div>
          <div>I rendered {renderCount} times</div>
        </>
      );
    };
    
    export default App;

     

    input에 작성할 때마다 렌더링을 몇번하는지 알려주도록 작성하고자 합니다. 위 코드를 보면 될듯 싶지만 무한으로 렌더링을 합니다.

    위 코드를 실행하면 아래와 같이 나옵니다.

     

    위 문제를 해결하기 위해 ueRef()를 사용할 수도 있습니다.

     

    해결법 #1

    useEffect(() => {
      setRenderCount((prevRenderCount) => prevRenderCount + 1);
    }, [name]);

    본래 이렇게 해결할 수 있습니다만 useRef()를 공부하고자 사용해보겠습니다.

     

    해결법 #2

    import React, { useEffect, useState, useRef } from "react";
    
    const App = () => {
      const [name, setName] = useState("");
      const renderCount = useRef(1);
    
      useEffect(() => {
        renderCount.current = renderCount.current + 1;
      });
    
      return (
        <>
          <input value={name} onChange={(e) => setName(e.target.value)} />
          <div>My name is {name}</div>
          <div>I rendered {renderCount.current} times</div>
        </>
      );
    };
    
    export default App;

    결과는 아래와 같습니다

    원하는대로 정상적으로 작동합니다!

     

    ForwardRef

    리액트를 사용하다보면 button, input 등의 컴포넌트를 재사용하기 위해 코드를 작성하는 경우가 많습니다. 아래는 어느 버튼 컴포넌트입니다.

    function FancyButton(props) {
      return (
        <button className="FancyButton">
          {props.children}
        </button>
      );
    }

    어떻게 작성을 해야 해당 컴포넌트가 ref를 child한테 전달을 할 수 있도록 할까요?

    React.forwardRef를 사용하면 해결이 됩니다.

    const FancyButton = React.forwardRef((props, ref) => (
      <button ref={ref} className="FancyButton">
        {props.children}
      </button>
    ));
    
    // 이제 DOM 버튼으로 ref를 작접 받을 수 있습니다.
    const ref = React.createRef();
    <FancyButton ref={ref}>Click me!</FancyButton>;

    forwardRef을 통해 일부 컴포넌트가 수신한 ref를 조금 더 아래로 전달할 수가 있습니다.

     

    주의사항

    • React.forwardRef를 사용 시 사용하기 전에 비해 리액트가 동작하는 방법이 크게 바뀔 가능성이 높아지며 이전 동작에 의존하는 앱이나 다른 라이브러리들이 손상될 가능성도 높아집니다.
    •  이 때문에 조건적으로 React.forwardRef가 존재할 때 조건부로 적용하는 것을 권장하지 않습니다.

     

    참고자료

     

    Ref와 DOM – React

    A JavaScript library for building user interfaces

    ko.reactjs.org

     

    Forwarding Refs – React

    A JavaScript library for building user interfaces

    ko.reactjs.org

     

    Hooks API Reference – React

    A JavaScript library for building user interfaces

    ko.reactjs.org

     

    '리액트' 카테고리의 다른 글

    클래스101 클론 후기  (0) 2022.01.16
    react-router-dom  (0) 2021.09.15
    Portals  (0) 2021.09.08
    Context  (0) 2021.09.05
    Lists & Keys & Reconciliation  (0) 2021.08.29
Designed by Tistory.