이 글은 React의 공식웹사이트의 Refs and the DOM을 번역한 글입니다. 마지막에 간단하게 정리해놓았으니 참고하시길 바랍니다.

Refs and the DOM

Refs는 DOM 노드나 render 메소드로 생성된 React element에 접근하는 방식을 제공한다.

일반적인 React 데이터 흐름에서, props가 부모 컴포넌트가 자식 컴포넌트와 상호작용 할 수 있는 유일한 수단이다. 자식 컴포넌트를 수정하기 위해, 새로운 props로 re-render해야한다. 하지만 일반적인 데이터 흐름 밖에서 자식 컴포넌트를 수정할 필요가 있을 것이다. 수정해야할 자식 컴포넌트는 리액트 컴포넌트의 인스턴스 일 수 도 있고 DOM element일 수도 있다. 두가지 경우 모두, 리액트는 탈출 창구(escape hatch)를 제공한다.

언제 Refs를 사용하나

refs를 사용하는 몇가지 경우들이 있다.

  • 포커스 관리, 텍스트 선택 혹은 미디어 플레이백
  • 중요한 애니메이션의 트리거
  • 서드파티 DOM 라이브러리의 통합

선언적으로 사용되는 어떤 것이든 refs사용을 피하자

예를들어, Dialog 컴포넌트의 open()과 close() 메소드를 노출하기 보다는, isOpen 프로퍼티를 주입하다록 하자.

Refs를 남용하지 말자

refs를 쓰기전에 컴포넌트의 상하관게에서 어디에 state를 가지는지 잘 생각해봐야 한다.
state를 "소유"하기 좋은 곳은 state가 컴포넌트의 상위레벨에 있을 때이다.

아래 예제는 리액트 16.3에 소개된 React.createRef() API를 사용하도록 업데이트 되었다.

Refs 생성

Refs는 React.createRef() 를 사용하여 생성하고 ref attribute 사용하여 React element에 붙힌다. Ref는 컴포넌트가 생성될 때 보통 인스턴스 property에 할당된다. 그래서 컴포넌트 전 영역에서 참조될 수 있게된다.

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

Refs에 접근

ref가 render 메소드의 element로 전해지면, 노드에 대한 참조는 ref의 currnt 어트리뷰트로 접근가능해진다.

const node = this.myRef.current;

ref의 값은 node의 타입에 따라 달라진다.

  • ref 어트리뷰트가 HTML element에 사용될 때, React.createRef() 에 의해 생성된 ref는 DOM 엘리먼트를 current 프로퍼티로 받는다.
  • ref 어트리뷰트가 사용자가 만든 클래스 컴포넌트에서 사용되면, ref 객체는 마운트된 컴포넌트의 인스턴스를 current로 받는다.
  • 함수 컴포넌트에 있는 ref 어트리뷰트를 쓰지 않아야 할 것이다. 왜냐하면 인스턴스를 가지고 있지 않기 때문이다.

DOM에 Ref 추가하기

이 코드는 reference를 DOM 노드에 저장하기 위해 ref를 사용하였다.

class CustomText Input extends React.Component {
    constructor(props) {
        super(props)
        // textInput DOM 엘리먼트를 저장하기위해 ref 생성
        this.textInput = React.createRef();
        this.focusTextInput = this.focusTextInput.bind(this)
    }

    focusTextInput(){
    // raw DOM API를 사용하여 text input을 포커스
    // 주의 : DOM node를 얻기위해 "current"로 접근하고 있다.
           this.textInput.current.focus();
    }

    render() {
        // 리액트에게 생성자 안의 textㅑnput으로 <input> ref와 연결하고 싶다고 말한다. 
        return (
        <div>
            <input type="text" ref={this.textInput} />
            <input type="button" value="Focus the text input" onClick={this.cofusTextInput} />
        </div>
        );
    }
}

리액트는 컴포넌트가 마운트될 때 DOM 엘리먼트와 함께 current 프로피티를 할당하고, 언마운트 될 때 null로 재할당한다. ref는 라이프 사이클의 componentDidMount 혹은 componentDidUpdate 이전에 업데이트가 된다.

class 컴포넌트에 refs 추가하기

만약 CustomTextInput 를 마운팅 직후 클릭했다고 가정했을 때, 커스마이즈한 input에 접근하기 위해 ref를 사용하고 focusTextInput 메소드를 수동으로 부를 수 있다.

class AutoFocusTextInput extends React.Component {
    constructor(props) {
        super(props);
        this.textInput = React.createRef();
    }

    componentDidMount() {
        this.textInput.current.focusTextInput();
    }

    render() {
        return (
            <CustomTextInput ref={this.textInput} />
        );
      }
}

CustomTextInput이 클래스로 선언된 경우에만 작동함을 주의하자

class CustomTextInput extends React.Compoenet {
    //..
}

Refs와 함수형 컴포넌트

함수형 컴포넌트는 인스턴스가 없기 때문에 함수형 컴포넌트에 ref 속성을 사용할 수 없다

function MyFunctionComponent() {
    return <input />;
}

class Parent extends React.Component {
    constructor(props) {
        super(props);
        this.textInput = React.createRef();
      }
    render() {
        // This will *not* work!
        return (
              <MyFunctionComponent ref={this.textInput} />
        );
      }
}

만약 ref가 필요하면 라이프사이클 메소드state가 필요했던 것 처럼 컴포넌트를 클래스로 바꿔야 될 것이다.

하지만, DOM 요소클래스 컴포넌트를 refer하는 한 그냥 함수형 컴포넌트 안에 ref 속성을 사용하면 된다.

function CustomTextInput(props) {
    // textInput가 ref가 참조될 수 있도록 반드시 여기에 정의되어야한다
    let textInput = React.createRef();

    function handleClick() {
        textInput.current.focus();
    }

    return (
        <div>
            <input type="text" ref={textInput} />
            <input type="button" value="Focus the text input" onClick {handleClick}/>
          </div>
    );
}

부모 컴포넌트에 DOM Refs 노출시키기

특이한 케이스로, 부모 컴포넌트에서 자식 DOM 노드로 접근하고 싶을 수도 있다. 컴포넌트의 캡슐화를 망쳐서 보편적으로는 추천되지 않는 방법이지만, 떄때로 포커스를 트리거하거나 사이즈를 측정하거나 자식 DOM 노드의 포지션을 구할 때 유용하게 사용된다.

자식 컴포넌트에 ref를 추가하면되지만, 이상적인 해결책은 아니더라도 DOM 노드를 받지말고 컴포넌트 인스턴스를 받는 것이 좋다. 더구나 함수형 컴포넌트에서는 먹히지도 않을 것이다.

만약 리액트 16.3이나 그 이상의 버전을 사용한다면, ref 포워딩을 사용하도록 추천한다. Ref 포워딩은 컴포넌트에게 자식 컴포넌트의 ref를 자신의 것처럼 선택적으로 노출하도록 하게 만든다. ref 포워딩 문서에서 자식의 DOM 노드를 부모 컴포넌트로 노출 시키는 예제를 볼 수 있다.

만약 리액트 16.2나 그 이하 버전을 사용하거나 ref 포워딩보다 유연한 방법이 필요하다면, 이 방법을 사용하고 다른 이름의 prop으로 ref를 넘기면 된다.

가능하다면 DOM 노드를 노출하지 말라고 하고 싶다. 하지만 꽤 괜찮은 탈출구가 될 수 있다. 이 방법은 자식 컴포넌트에 코드를 좀 더 넣어야 될 것이다. 만약 자식 컴포넌트를 아에 컨트롤 할 수 없는 상황이라면, 마지막 옵션은 findDOMNode() 를 사용하는 것이다. 하지만 추천하지는 않고 String Mode에서 사라졌다.

콜백 ref

리액트는 또한 ref를 설정하는 또 다른 방법으로 "콜백 ref"를 제공한다. 이것은 ref를 설정하거나 그렇지 않을 때 조금 더 괜찮은 방법으로 제어를 할 수 있다.

createRef() 로 만든 ref 속성을 넣는 대신, 함수를 넣으면 된다. 함수는 리액트 컴포넌트 인스턴스HTML DOM 요소를 아규먼트로 받는데 이는 저장을 하거나 다른 곳에서 접근이 가능하다.

아래의 예는 보편적인 패턴을 사용한 것이다. 인스턴스의 propertyDOM 노드의 참조값을 저장하기 위해 ref 콜백을 사용했다.

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

        this.textInput = null;

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

        this.focusTextInput = () => {
            // Focus the text input using the raw DOM API
            if (this.textInput) this.textInput.focus();
        };
      }

    componentDidMount() {
            // input이 마운트 되면 자동 포커스
        this.focusTextInput();
      }

    render() {
        // 인스턴스 필드의 텍스트 input DOM요소에 참조값을 저장하기 위해 'ref' 콜백을 사용했다. (에를 들어, this.textInput)
        return (
              <div>
                <input type="text" ref={this.setTextInputRef}/>
                  <input type="button" value="Focus the text input" onClick={this.focusTextInput} />    
              </div>
        );
      }
}

리액트는 컴포넌트가 마운트 될 때 ref 콜백을 DOM 요소와 함께 부른다. 컴포넌트가 언마운트되면 ref 콜백을 null과 함께 부른다. refs는 componentDidMountcomponentDidUpdate가 호출되기 전까지 항상 최신 상태가 보장된다.

이전에 생성한 React.createRef() 로 생성한 refs 객체로 넣어준 것 처럼 컴포넌트 사이에 콜백 ref를 넣어줄 수 있다.

function CustomTextInput(props) {
    return (
        <div>
              <input ref={props.inputRef} />
        </div>
      );
}

class Parent extends React.Component {
      render() {
        return (
          <CustomTextInput inputRef={el => this.inputElement = el}/>
        );
      }
}

위의 예제를 보면, 부모는 부모의 ref 콜백을 inputRef prop으로 CustomTextInput에게 보내고, CustomTextInput은 같은 함수를 ref속성으로 input 태그로 보낸다. 결론적으로 Parent의 this.inputElement는 CustomTextInput의 input 요소와 대응되는 DOM 노드로 세팅된다.

정리

ref를 이용하여 DOM 노드를 가져올 수 있다.

  1. React.createRef()
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}
  1. 콜백 ref
class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;
  }

  render() {
    return (
      <div>
        <input type="text" ref={element => this.textInput = element}/>
      </div>
    );
  }
}

부모 컴포넌트에서 자식 컴포넌트에게 ref를 보내는건 추천하는 방법은 아니지만 할 수는 있다.

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

지역변수

const variable = useRef(1)
const result = variable.current

결과

result = 1

ref

const variable = useRef()
<div ref={variable}>

const result = variable.current

결과

<div></div>

참조

Refs in React : All you need to know ! 이 글 또한 꽤 자세하게 다루고 있다. 앞서 리액트 공식 도큐먼트에서 제시한 React.createRef() 방법이 아닌, ref="이름" 의 형식으로 ref를 다루는 방법을 기술하고 있다. 하지만 공식문서에서 deprecate이 된 것은 아니지만 될 수도 있으니 콜백 ref를 사용하는 것을 한번 더 언급하고 있다.