#react,  #ref

React ref 톺아보기

React ref 톺아보기

0. Intro

React로 웹프론트엔드 개발을 하다보면 React만으로는 DOM을 조작하기 어려울 때가 있습니다. 가장 흔하게는 어떤 엘리먼트를 focus 해야 할 때 말이죠. React의 ref는 무엇이고, ref를 사용하는 이유에 대해 이야기해보는 시간을 가져보려고 합니다.

1. ref가 뭘까?

reference의 정의 - 네이버 영어 사전

ref는 reference의 준말입니다. 한국말로는 참고, 참조 정도가 React ref의 의미를 잘 나타냅니다.

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

ref는 사실 일반 객체입니다.
ref를 console.log로 찍어보면 current 프로퍼티 하나를 가진 객체가 나타납니다.

image

React는 이 객체를 통해 DOM에 직접적인 접근을 가능케 해줍니다.

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

  componentDidMount() {
    console.log(this.ref.current.style.backgroundColor);
  }

  render() {
    return (
      <div ref={this.ref} style={{ backgroundColor: 'red', width: '100px', height: '100px' }} />
    );
  }
}

위와 같이 createRef 함수를 통해 ref를 생성하면, ref 객체를 React Element의 ref prop에 전달합니다.
그러면 함수가 반환한 객체의 current 프로퍼티로 해당 React Element의 DOM에 접근을 할 수 있게 됩니다.

위 코드의 결과는 다음과 같습니다.

image

2. ref의 활용처: 비제어 컴포넌트

ref는 언제 활용할 수 있을까요? 공식문서에서는 아래와 같이 정의합니다.

  • JS로 DOM 요소에 focus 하기, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때.
  • 애니메이션을 직접적으로 실행시킬 때.
  • 서드 파티 DOM 라이브러리를 React와 같이 사용할 때.

저는 위의 경우들을

비제어 컴포넌트를 제어할 때

라고 일축할 수 있을 것 같습니다.

React 시스템 안에서 제어하지 않고, 순수 JS를 이용해 제어하는 컴포넌트를 비제어 컴포넌트라고 합니다.
비제어 컴포넌트라는 말이 어렵다면, React가 제어하지 않는 컴포넌트라고 기억하면 좋을 것 같아요.

React 시스템을 이용하지 않는다는 이야기는 React가 제공하는 재조정과 같은 feature들을 이용하지 않음을 의미합니다.

비제어 컴포넌트를 순수 JS로 제어하기 위해서, 브라우저상의 DOM 노드를 담는 역할을 ref가 하는 것입니다.

예를 하나 들어보겠습니다.

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

    this.state = {
      value: ""
    };
  }

  render() {
    return (
      <input
        value={this.state.value}
        onChange={({ target: { value } }) =>
          this.setState({ ...this.state, value })
        }
      />
    );
  }
}

위 코드는 일반적인 React의 상태로 제어하는 제어 컴포넌트입니다.
state로 input의 value를 관리하기 때문에 input에서 키보드 입력을 할 때마다 state가 변하게 되고,
state가 변함에 따라, UI를 업데이트하기 위해 re-rendering이 계속 일어납니다.

controlled-component

입력할 때마다 테두리가 노란색으로 변하는 것이 보이시나요? re-rendering이 일어나고 있는 것입니다.
혹자는 이런 프로세스가 불필요한 컴퓨팅 자원을 소모하는 이슈라고 판단할 수도 있습니다.
그러면 React 시스템이 해당 요소를 관리하지 않도록 해주면 문제가 해결됩니다.

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

    this.ref = React.createRef();
  }

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

ref가 input을 가리키도록 하였습니다.
이제 input은 state를 변경하지 않기에 불필요한 re-rendering은 일어나지 않을 것입니다.

바로 확인해 볼까요?

uncontrolled-component

제어컴포넌트에선 보이던 파랑/노랑 테두리 선이 보이지 않습니다.
re-rendering이 일어나지 않고 있는 것입니다.

이제 ref를 언제, 왜 사용하는지는 알 수 있는 것 같습니다.
하지만 DOM API가 있는데 왜 굳이 써야 되고, 또 왜 이런 형태일까요?

3. 왜 current로 접근해야 하나?

querySelector처럼 그냥 DOM 요소를 반환해주면 좋을 텐데,
왜 createRef/useRef는 왜 객체를 반환하고 current 프로퍼티로 DOM 요소를 전달해줄까요?

이는 React가 가상 돔을 기반으로 작동하는 라이브러리라는 사실을 생각해보면 이유가 명확해집니다.

공식문서는 이렇게 설명합니다.

컴포넌트가 마운트될 때 React는 current 프로퍼티에 DOM 엘리먼트를 대입하고, 컴포넌트의 마운트가 해제될 때 current 프로퍼티를 다시 null로 돌려놓습니다. ref를 수정하는 작업은 componentDidMount 또는 componentDidUpdate 생명주기 메서드가 호출되기 전에 이루어집니다.

실제 DOM에 React 노드가 렌더될 때까지 ref가 가리키는 DOM 요소의 주소 값은 확정된 것이 아닙니다.

즉 우리가 ref에 접근할 수 있는 시점은 React 노드가 실제로 DOM에 반영되는 시점부터입니다.
(라이프사이클상에서는 componentDidMount) 그 이전에는 null이 current 프로퍼티에 담깁니다. (useref는 초깃값을 따로 인자로 전달해줄 수 있습니다.)

그리고 가상 DOM이 변경될 때 실제 DOM의 요소도 변경되는 경우가 있기 때문에 DOM이 업데이트되는 경우(componentDidUpdate)도 ref의 current 값이 변경되게 됩니다.
이처럼 유동적이기에 React는 객체를 반환해 current 프로퍼티의 값을 계속해서 수정합니다.

4. 왜 DOM API를 쓰면 안 될까?

또 궁금증이 생깁니다. 그냥 DOM API로 요소에 접근하면 안 될까?
DOM API는 React에서 동작하지 않을까요? 동작합니다.
위 예제를 바꿔보겠습니다.

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

  componentDidMount() {
    console.log('querySelector!: ', document.querySelector('#hi').style.backgroundColor);
  }

  render() {
    return <div id="hi" style={{ backgroundColor: 'red', width: '100px', height: '100px' }} />;
  }
}

결과는 아래와 같습니다.

image

잘 작동합니다. 그러면 왜 되도록 ref를 써야 하는 걸까요?

ref는 특정 DOM 요소를 가져올 때 더 신뢰할 만하기 때문입니다.
1번 내용처럼 라이프사이클에 따라 DOM 요소를 가져오지 못하는 경우가 있습니다.
예측하지 못한 상황(대개는 라이프사이클의 흐름을 예측하지 못한 상황)으로 DOM 요소를 가져오지 못한다면
이는 해당 코드가 포함된 로직에 따라 큰 결함으로 이어질 수 있을 것입니다.

또 만약 컴포넌트가 하나가 아닌 여러 개가 생성되는 경우를 생각해 봅시다.
이때 우리는 id나 class로 특정해서 원하는 DOM 요소를 가져올 수 있을까요? 분명 쉽지 않을 것입니다.
DOM 요소를 특정할 수 있도록 관심 영역을 특정 컴포넌트로 제한하는 역할도 ref가 할 수 있습니다.

5. ref 객체를 만드는 또 다른 방법, callback ref

지금까지는 ref를 생성하는 방법을 createRef로만 사용했습니다.
React는 createRef 외에 또 하나의 방법을 제공하는데 그 이름은 callback ref입니다.

사용법은 간단합니다.

별도의 API가 제공이 되는 것은 아니고, callback, ref prop에 ref 객체를 인스턴스 내 멤버 변수에 할당하는 callback 함수를 전달하면, react가 해당 멤버 변수에 ref 객체를 전달해 줍니다.

class Example extends React.Component {
  ref = null;

  setDivref(element) {
    this.ref = element;
    console.log(this.ref.style.backgroundColor);
  }

  render() {
    return (
      <div
        ref={this.setDivref}
        style={{ backgroundColor: 'red', width: '100px', height: '100px' }}
      />
    );
  }
}

callback 함수로 this.ref에 ref 객체를 할당하고 전달받은 element의 background를 콘솔에 출력하는 예제입니다.

image

위와 같이 잘 출력이 되는 것을 확인할 수 있습니다.
callback ref는 함수를 ref에 전달해 ref가 할당될 당시 수행해야 할 동작이 있을 때 사용할 수 있을 것입니다.

6. 함수 컴포넌트에서 ref 사용하기, useRef와 forwardRef

지금까지 클래스 컴포넌트에서 ref 사용하는 법을 설명해봤습니다.
요즘은 함수 컴포넌트가 많이 쓰이지 않나요?
함수 컴포넌트에서도 ref를 이용할 수 있습니다.
다만 상태가 바뀔 때마다 매번 새롭게 호출되는 함수 컴포넌트의 특성상, 기존의 방식과는 다른 방법이 요구됩니다.

1. createRef() VS useRef()

함수 컴포넌트에서 createRef를 사용할 수는 있습니다.
하지만 앞서 말한 것처럼 함수 컴포넌트는 상태가 바뀔 때마다 될 때마다 새롭게 호출되기에,
ref가 가리키는 DOM 요소가 re-render 되는 것과 상관없이 새로운 ref 객체가 계속 만들어지게 됩니다.

const Example = () => {
  const ref = createRef(null);
  const [shouldRerender, setShouldRerender] = useState(false);

  useEffect(() => {
    console.log(ref);
  }, [ref]);

  const rerender = () => {
    setShouldRerender(!shouldRerender);
  };

  return (
    <div>
      <div ref={ref} style={{ backgroundColor: 'red', width: '100px', height: '100px' }} />
      <button onClick={rerender}> rerender</button>
    </div>
  );
};

image

re-render 버튼을 누르면 해당 컴포넌트의 상태가 바뀌기 때문에 함수 컴포넌트가 다시 호출됩니다.
하지만 빨간 박스는 변경점이 없기에 re-rendering이 일어나지 않습니다.
하지만 createRef는 계속 호출되기에 콘솔 창에는 ref에 대한 내용이 클릭할 때마다 출력됩니다.

image

const Example = () => {
  const ref = useRef(null);
  const [shouldRerender, setShouldRerender] = useState(false);

  useEffect(() => {
    console.log(ref);
  }, [ref]);

  const rerender = () => {
    setShouldRerender(!shouldRerender);
  };

  return (
    <div>
      <div ref={ref} style={{ backgroundColor: 'red', width: '100px', height: '100px' }} />
      <button onClick={rerender}>rerender</button>
    </div>
  );
};

하지만 이를 useRef로 바꾼다면?

useRef는 이름에서 알 수 있듯이 hook으로 함수 호출에 관계없이 state를 유지합니다.
동작이 예상 가시나요?

image

몇 번을 클릭해도 ref의 값은 유지되기에 콘솔 창에는 한 번만 출력되게 됩니다.
함수 컴포넌트를 사용할 때는 useRef를 사용해야 할 것입니다.

2. 함수 컴포넌트는 ref를 포워딩할 수 있다. forwardRef

포트 포워딩이란 네트워크 개념에 대해 많이들 알고 계실 것입니다.
포트 포워딩은 외부에 특정 포트를 공개해, 외부에서 해당 포트로 접속하면 미리 지정했던 내부 IP의 특정 포트로 연결시켜주는 기능입니다.

forwardRef도 똑같습니다. 함수 컴포넌트에서 특정 요소에 ref를 전달하고 싶은 경우가 있을 수 있습니다.
가장 흔한 케이스는 input 태그를 스타일링하기 위해 컴포넌트로 래핑 했는데, 사용자 경험 향상을 위해 프로그램적으로 input에 focus를 하고 싶을 때입니다.

ref를 prop으로 받아서 원하는 element에 전달해 주면 되지 않을까요?

function App() {
  const ref = useRef(null);

  useEffect(() => {
    console.log(ref);
  }, [ref]);

  return (
    <div className="App">
      <StyledInput ref={ref} />
    </div>
  );
}

const StyledInput = ({ ref }) => {
  return <input ref={ref} />;
};

export default App;

image

땡! React는 에러를 매섭게 뱉어냅니다.
자식에게 전달되는 prop이 아니라네요. 비슷한 케이스로 key prop이 있죠.

export default function App() {
  const ref = useRef(null);

  useEffect(() => {
    console.log(ref);
  }, [ref]);

  return (
    <div className="App">
      <StyledInput inputref={ref} />
    </div>
  );
}

const StyledInput = ({ inputref }) => {
  return <input ref={inputref} />;
};

image

사실 forwardRef라는 기능 없이도 이렇게 별도의 prop으로 전달해 줄 수 있습니다.
하지만 ref prop으로 전달해 줄 수 있다면, StyledInput의 코드를 보지 않고도 동작을 예상할 수 있게 되어 보다 더 직관적인 코드가 될 수 있을 것입니다.

이때 forwardRef라는 HOC를 사용합니다.
이 HOC로 컴포넌트를 감싸면 컴포넌트의 두 번째 인자로 ref를 전달을 할 수 있게 됩니다.

function App() {
  const ref = useRef(null);

  useEffect(() => {
    console.log(ref);
  }, [ref]);

  return (
    <div className="App">
      <StyledInput ref={ref} />
    </div>
  );
}

const StyledInput = forwardRef((props, ref) => {
  return <input ref={ref} />;
});

image

역시 콘솔 창에 로그가 출력이 되는 것을 확인할 수 있습니다.

마무리

이상으로 ref에 대해 알아보았습니다.

풀리지 않은 궁금증이 있다면 댓글로 남겨주시면 해당 부분에 대한 보충을 할 수 있도록 하겠습니다.

감사합니다.

reference