React Ref

Ref란 뭘까?

HTML에서 id를 사용하여 DOM에 이름을 달아 접근하는 것처럼 리액트 프로젝트에서는 ref(reference)를 사용하여 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근한다.
그렇다면 왜 ref를 사용하여 DOM에 접근하는 걸까?
id를 사용할 수도 있지만 가급적 사용하지 않고 ref를 사용하는 이유는 DOM의 id는 유일해야 하는데, 컴포넌트를 여러 번 사용할 경우 중복 id가 발생할 수도 있기 때문이다. ref는 전역적으로 작동하지 않고 컴포넌트 내부에서만 작동하기 때문에 이러한 문제가 생기지 않는다.
하지만 ref를 사용하기 전에 ref를 사용하지 않고도 원하는 기능을 구현할 수 있는지 충분히 고려한 후에 ref를 사용하는 것이 좋다. ref를 사용해야 하는 상황은 다음과 같다.

ref를 사용해야 하는 상황(DOM을 직접적으로 건드려야할 때)

  • 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때.
  • 애니메이션을 직접적으로 실행시킬 때.
  • 서드 파티 DOM 라이브러리를 React와 같이 사용할 때.
  • 스크롤 박스 조작할 때.
  • Canvas 요소에 그림을 그릴 때.

ref를 사용하는 방법에는 두 가지가 있다.

1. 콜백 함수를 통한 ref 설정

ref를 달고자 하는 요소에 ref라는 콜백 함수를 props로 전달해 주면 된다. 이 콜백 함수는 ref 값을 파라미터로 전달받아서 함수 내부에서 파라미터로 받은 ref를 컴포넌트의 멤버 변수로 설정해 준다.

<input ref={(ref) => {this.textInput=ref}}>

위와 같이 작성하면 this.textInput은 input 요소의 DOM을 가리킨다.

아래와 같이 작성할 수도 있다.(리액트 공식 문서 참조)

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 콜백을 인라인 함수로 선언하였다면 ref 콜백은 업데이트 과정 중에 처음에는 null로, 그 다음에는 DOM 엘리먼트로, 총 두 번 호출된다. 이러한 현상은 매 렌더링마다 ref 콜백의 새 인스턴스가 생성되므로 React가 이전에 사용된 ref를 제거하고 새 ref를 설정해야 하기 때문에 일어난다. 이러한 현상은 ref 콜백을 클래스에 바인딩된 메서드로 선언함으로써 해결할 수 있지만 대부분 이런 현상은 문제가 되지 않는다.

2. createRef를 통한 ref 설정

createRef는 리액트 16.3 버전부터 추가되었다. 이 함수를 사용해서 코드를 작성하면 더 적은 코드로 쉽게 사용할 수 있다. 사용법은 아래와 같다.

class RefSample extends Component {
  textInput = React.createRef();

  handleFocus = () => {
    this.textInput.current.focus();
  };

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

콜백 함수를 사용할 때와 다른 점은 뒷부분에 .current를 넣어 주어야 한다는 것이다. React.createRef()로 생성된 ref는 자신을 전달받은 DOM 엘리먼트를 current 프로퍼티의 값으로서 받는다.

함수형 컴포넌트는 인스턴스가 없기 때문에 함수 컴포넌트에서는 ref Attribute를 사용할 수 없다. 함수형 컴포넌트에서는 useRef라는 Hook 함수를 사용한다.

컴포넌트에 ref 달기(부모 컴포넌트에게 DOM ref 공개)

컴포넌트에 ref를 달고 부모 컴포넌트에서 자식 컴포넌트의 내부 메서드를 사용할 수 있다.

ScrollBox.js

class ScrollBox extends Component {
  scrollToBottom = () => {
    const {scrollHeight, clientHeight} = this.box;

    this.box.scrollTop = scrollHeight - clientHeight;
  }

  render() {
    const style = {
      border:'1px solid black',
      height:'300px',
      width:'300px',
      overflow:'auto',
      position:'relative'
    };

    const innerStyle = {
      width: '100%',
      height: '650px',
      background: 'linear-gradient(white, black)'
    }

    return (
      <div
        style={style}
        ref={(ref) => {this.box = ref}}>
        <div stle={innerStyle}>
      </div>
    )
  }
}

App.js

class App extends Component {
  render() {
    return (
      <div>
        <ScrollBox ref={ref => (this.scrollBox = ref)} />
        <button onClick={() => this.scrollBox.scrollToBottom()}>
          맨 밑으로
        </button>
      </div>
    );
  }
}

위 코드에서 주의할 점은 onClick={this.scrollBox.scrollToBottom} 같은 형식으로 작성해도 틀린 것은 아니지만 컴포넌트가 처음 렌더링될 때는 this.scrollBox 값이 undefined 이므로 값을 읽어오는 과정에서 오류가 발생한다. 화살표 함수를 사용하여 새로운 함수를 만들고 그 내부에서 this.scrollBox.scrollToBottom 메서드를 실행하면, 버튼을 누를 때 이미 한번 렌더링을 했기 때문에 오류가 발생하지 않는다.

ref를 사용할 때에는 먼저 ref를 사용하지 않고도 원하는 기능을 구현할 수 있는지 반드시 고려한 후에 ref를 사용하도록 한다. 서로 다른 컴포넌트끼리 데이터를 교류할 때 ref를 사용한다면 이는 잘못 사용된 것이다. 이는 리액트의 사상에 어긋난 설계이고 앱 규모가 커질수록 구조가 꼬여 유지 보수하기가 점점 힘들어진다.
컴포넌트끼리 데이터를 교류할 때는 언제나 데이터를 부모 <-> 자식 흐름으로 교류해야 한다.


reference:

리액트 공식 문서