Frontend/React

forwardRef - 함수 컴포넌트에서의 ref 속성 사용

둉이 2022. 3. 16. 01:44

** 이 글은 react 16.2 버전 이후 기준으로 작성되었습니다. **

 

 


 

 

리액트에서는 직접적으로 DOM 요소에 접근하는 것을 권장하지 않는다.

 

부득이하게 DOM 요소에 접근해야 할 때에는 ref 속성을 이용하여 특정 DOM 요소에 접근할 수 있다.

 

클래스형 컴포넌트에서는 createRef, 함수 컴포넌트에서는 useRef를 사용하여 ref 변수를 생성할 수 있다.

 

함수 컴포넌트에서도 createRef를 사용할 수는 있지만, 컴포넌트가 리렌더링될 때마다 ref 값이 null로 초기화되어 화면에 표출되는 원하는 값을 얻을 수 없으므로 useRef를 사용하는 것을 권장한다.

import React, { useState, createRef } from 'react';

const App = () => {
  const ref = createRef(0);
  const [_, setState] = useState(false);
  const handleClick = () => {
    ref.current = 123;
    setState(true);
    console.log(ref.current);  // 123
  };
  return (
    <>
      <span>{ref.current}</span> {/* 123이 표출되지 않음 */}
      <button onClick={handleClick}>증가</button>
    </>
  );
};

export default App;

 

 

ref 속성은 다음과 같은 상황에 주로 사용된다.

 

  • input 요소의 속성(value, focus 등)에 접근해야 할 때
  • DOM 요소의 애니메이션을 직접적으로 실행해야 할 때
  • 서드 파티 DOM 라이브러리를 사용할 때

 

 

사용 방법은 다음과 같다.

 

ref가 가리키는 DOM 요소에 접근할 때는 ref의 current 속성으로 접근할 수 있다.

 

- 클래스형 컴포넌트

import { Component, createRef } from 'react';

class index extends Component {
  constructor() {
    super();
    this.ref = createRef();
  }

  handleClick() {
    console.log(this.ref.current.value);
  }

  render() {
    return (
      <main>
        <input type="text" ref={this.ref} />
        <button type="button" onClick={this.handleClick.bind(this)}>
          Enter Text
        </button>
      </main>
    );
  }
}

export default index;

 

- 함수 컴포넌트

import { useRef } from 'react';

const Main = () => {
  const ref = useRef();

  const handleClick = () => {
    console.log(ref.current.value);
  };

  return (
    <main>
      <input type="text" ref={ref} />
      <button type="button" onClick={handleClick}>
        Enter Text
      </button>
    </main>
  );
};

export default Main;

 

 

그럼 위 예시처럼 일반적인 HTML 태그가 아닌 커스텀 컴포넌트에 ref를 적용하고자 할 때는 어떻게 하면 될까?

 

간단하게 부모 컴포넌트에서 useRef 값을 props로 전달하여 해당 자식 컴포넌트의 ref 속성에 넘겨주는 방식을 생각할 수 있다.

 

하지만 실제로 이런 방식은 클래스형 컴포넌트에서는 문제가 없지만, 함수 컴포넌트로 구현하게 되면 ref.current에 접근했을 때 다음과 같은 오류를 겪게 된다.

 

함수 컴포넌트는 클래스형 컴포넌트와는 달리 인스턴스가 없기 때문에 ref 속성을 사용할 수 없기 때문이다.

커스텀 컴포넌트에 ref 속성을 전달했을 때의 Warning

 

 

함수 컴포넌트에서 ref를 커스텀 컴포넌트에 전달하여 사용하고자 할 때는 forwardRef라는 함수를 사용해야 한다.

 

사용법은 간단하다.

 

1. ref를 지정하고자 하는 커스텀 컴포넌트에 ref 속성을 추가한다.

const emailRef = useRef();

<Input
  id="email"
  ref={emailRef}
  label="E-Mail"
  value={enteredEmail}
  isValid={emailIsValid}
  onChange={emailChangeHandler}
  onBlur={validateEmailHandler}
/>

 

2. 커스텀 컴포넌트의 2번째 파라미터로 ref를 추가한다. (props, ref) 형태로 수정하면 된다.

 

3. 커스텀 컴포넌트 내 실제로 ref를 지정하고자 하는 요소에 ref 속성을 추가한다.

 

4. 해당 커스텀 컴포넌트를 export할 때, forwardRef 함수로 감싼다.

import { forwardRef } from 'react';

import classes from './Input.module.css';

const Input = ({ label, id, value, isValid, onChange, onBlur }, ref) => {
  return (
    <div className={`${classes.control} ${isValid ? '' : classes.invalid}`}>
      <label htmlFor={id}>{label}</label>
      <input type={id} ref={ref} id={id} value={value} onChange={onChange} onBlur={onBlur} />
    </div>
  );
};

export default forwardRef(Input);

 

 

forwardRef는 보통 useImperativeHandle과 함께 사용된다.

 

useImperativeHandle은 forwardRef를 사용하는 하위 컴포넌트에서 선언한 변수 혹은 함수를 상위 컴포넌트에서 실행할 수 있도록 하는 hook이다.

 

간단히 말하자면, 부모 컴포넌트에서 자식 컴포넌트의 값에 접근하거나 함수를 실행할 수 있다.

 

 

사용 방법은 다음과 같다.

 

첫 번째 인자로 전달 받은 ref, 두 번째 인자로 key: value 형태의 객체를 반환하는 함수를 전달하면 된다.

 

두 번째 인자에서 접근하는 ref는 useRef를 이용하여 별도의 ref를 생성하여 사용하면 된다.

 

이 때, 기존 하위 컴포넌트에서 사용하고 있던 ref 속성의 값도 새로 생성한 ref 변수로 변경해야 한다.

 

const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    setFocus: () => {
      inputRef.current.focus();
    },
  }));

  return (
    <div className={`${classes.control} ${isValid ? '' : classes.invalid}`}>
      <label htmlFor={id}>{label}</label>
      <input type={id} ref={inputRef} id={id} value={value} onChange={onChange} onBlur={onBlur} />
    </div>
  );

 

forwardRef와 useImperativeHandle을 모두 적용한 전체 코드는 다음과 같다.

import { useRef, forwardRef, useImperativeHandle } from 'react';

import classes from './Input.module.css';

const Input = ({ label, id, value, isValid, onChange, onBlur }, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    setFocus: () => {
      inputRef.current.focus();
    },
  }));

  return (
    <div className={`${classes.control} ${isValid ? '' : classes.invalid}`}>
      <label htmlFor={id}>{label}</label>
      <input type={id} ref={inputRef} id={id} value={value} onChange={onChange} onBlur={onBlur} />
    </div>
  );
};

export default forwardRef(Input);

 

참고로, 리액트에서는 되도록 ref를 사용하기 보다는 선언적으로 프로그래밍하는 것을 권장하고 있다.

 

반드시 필요한 경우에만 사용하도록 하자.