Logo

[React] forwardRef 사용법

React 컴포넌트에 ref prop을 넘겨서 그 내부에 있는 HTML 엘리먼트에 접근을 하게 해주는 forwardRef() 함수에 대해서 알아보겠습니다.

HTML 엘리먼트의 ref prop

React에서 ref prop은 HTML 엘리먼트의 직접 접근하기 위해서 사용됩니다.

예를 들어, 아래 <Field/> 컴포넌트에서는 useRef() 훅(hook) 함수로 생성한 inputRef 객체를 <input/> 엘리먼트의 ref prop으로 넘기고 있습니다. 이렇게 해주면 inputRef 객체의 current 속성에 <input/> 엘리먼트의 레퍼런스가 할당되는데, 이를 통해 handleFocus() 이벤트 핸들러에서 <input/> 엘리먼트의 focus() 함수를 호출할 수 있습니다.

import React, { useRef } from "react";

function Field() {
  const inputRef = useRef(null);

  function handleFocus() {
    inputRef.current.focus();
  }

  return (
    <>
      <input type="text" ref={inputRef} />
      <button onClick={handleFocus}>입력란 포커스</button>
    </>
  );
}

React의 ref prop에 대한 자세한 내용은 별도의 포스팅으로 다루었으니 참고 바랍니다.

React 컴포넌트의 ref prop

어떤 컴포넌트에서 다른 컴포넌트 내부에 있는 HTML 엘리먼트에 직접 접근해야할 때가 종종 있는데요.

예를 들어, 위 <Field/> 컴포넌트로 부터 <input> 엘리먼트를 별도의 <Input/> 컴포넌트로 빼내보겠습니다.

먼저 부모인 <Field/> 컴포넌트는 useRef() 훅(hook) 함수로 생성한 inputRef 객체를 자식인 <Input/> 컴포넌트에 ref prop으로 넘김니다. 그러면 자식인 <Input/> 컴포넌트는 이 ref prop으로 넘어온 inputRef 객체를 다시 내부에 있는 <input/> 엘리먼트의 ref prop으로 넘겨줍니다.

import React, { useRef } from "react";

function Input({ ref }) {
  return <input type="text" ref={ref} />;
}

function Field() {
  const inputRef = useRef(null);

  function handleFocus() {
    inputRef.current.focus();
  }

  return (
    <>
      <Input ref={inputRef} />
      <button onClick={handleFocus}>입력란 포커스</button>
    </>
  );
}

얼핏 그럴듯한 코드로 보이나 실제로 브라우저에서 실행을 해보면 콘솔에서 다음과 같은 경고 메시지를 보게 될 것입니다.

Warning: Input: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)
    at Input (https://nqxh4.csb.app/src/Field.jsx:18:18)
    at Field (https://nqxh4.csb.app/src/Field.jsx:30:36)
    at div
    at App

해석을 해보면 ref는 prop이 아니라서 undefined가 설정될 것이고, 그래서 다른 prop을 사용해야한다는 것인데요. 가이드대로 ref 대신에 ref1과 같이 다른 이름의 prop을 사용하도록 <Input/> 컴포넌트를 수정하여 이 문제를 해결할 수 있습니다.

하지만 이 컴포넌트의 사용자 측면에서 생각해보면 이러한 컴포넌트 디자인은 혼란스럽게 받아들여질 수 있습니다. 어떻게 하면 React 컴포넌트에서도 HTML 엘리먼트와 같이 refref라고 부를 수 있을까요? 😆

forwardRef

React에서 특수한 목적으로 사용되기 때문에 일반적인 용도로 사용할 수 없는 prop이 몇 가지 있습니다. 대표적인 예로 루프를 돌면서 동일한 컴포넌트 여러 번 랜더링할 때 사용하는 key prop을 들 수 있는데요. ref prop도 마찬가지로 HTML 엘리먼트 접근이라는 특수한 용도로 사용되기 때문에 일반적인 prop으로 사용을 할 수 없습니다.

HTML 엘리먼트가 아닌 React 컴포넌트에서 ref prop을 사용하려면 React에서 제공하는 forwardRef()라는 함수를 사용해야 합니다. React 컴포넌트를 forwardRef()라는 함수로 감싸주면, 해당 컴포넌트는 함수는 두 번째 매개 변수를 갖게 되는데, 이를 통해 외부에서 ref prop을 넘길 수 있습니다.

예를 들어, 위에서 작성한 <Input/> 컴포넌트에 forwardRef()라는 함수를 적용해보겠습니다.

import React, { forwardRef, useRef } from "react";

const Input = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

function Field() {
  const inputRef = useRef(null);

  function handleFocus() {
    inputRef.current.focus();
  }

  return (
    <>
      <Input ref={inputRef} />
      <button onClick={handleFocus}>입력란 포커스</button>
    </>
  );
}

이제 버튼을 클릭해보면 기존과 같이 입력란으로 포커스가 이동하는 것을 볼 수 있을 것입니다! 😄

예제: audio 엘리먼트 제어

좀 더 실제 프로젝트에서 사용할 법한 예로 audio 엘리먼트 제어하는 컴포넌트를 작성해보겠습니다.

먼저 <Audio/> 자식 컴포넌트와 <Controls/> 자식 컴포넌트로 이루어진 <Player/> 부모 컴포넌트를 작성하겠습니다.

// Player.jsx
import React, { useRef } from "react";
import Audio from "./Audio";
import Controls from "./Controls";

function Player() {
  const audioRef = useRef(null);

  return (
    <>
      <Audio ref={audioRef} />
      <Controls audio={audioRef} />
    </>
  );
}

export default Player;

useRef() 훅(hook) 함수로 audioRef 객체를 생성한 후, <Audio/> 컴포넌트에는 ref prop으로, <Controls/> 컴포넌트에는 audio prop으로 넘겨주고 있습니다.

<Player/> 컴포넌트에서 넘기는 ref prop을 제대로 받으려면 <Audio/> 컴포넌트는 forwardRef() 함수를 사용해야 합니다. 그러면 두 번째 매개 변수를 통해 ref 객체가 넘어오게 되고, 내부에 있는 <audio> 엘리먼트로 다시 넘겨(forward)줄 수 있습니다. 이를 통해 부모 컴포넌트인 <Player/>에서 자식 컴포넌트인 <Audio/>의 내부에 있는 <audio> 엘리먼트에 직접 접근할 수 있게 되었습니다.

// Audio.jsx
import React, { forwardRef } from "react";
import music from "./music.mp3";

function Audio(prop, ref) {
  return (
    <figure>
      <figcaption>Eyes on You (Sting) - Network 415:</figcaption>
      <audio src={music} ref={ref}>
        Your browser does not support the
        <code>audio</code> element.
      </audio>
    </figure>
  );
}

export default forwardRef(Audio);

<Controls/> 컴포넌트에는 ref가 아닌 audio라는 일반적인 prop으로 audioRef 객체가 넘기기 때문에 굳이 forwardRef() 함수를 사용할 필요가 없습니다. <Controls/> 컴포넌트 내의 이벤트 핸들러 함수는 <Player/> 컴포넌트로 부터 넘어온 audioRef 객체를 통해서 <audio> 엘리먼트의 play()pause() 함수를 호출할 수 있습니다.

// Controls.jsx
import React from "react";

function Controls({ audio }) {
  const handlePlay = () => {
    audio.current.play();
  };

  const handlePause = () => {
    audio.current.pause();
  };

  return (
    <>
      <button onClick={handlePlay}>재생</button>
      <button onClick={handlePause}>중지</button>
    </>
  );
}

export default Controls;

보너스: 디버깅 팁

forwardRef() 함수를 호출할 때 다음과 같이 익명 함수를 넘기면 브라우저에서 React 개발자 도구를 사용할 때 컴포넌트의 이름이 나오지 않아서 불편할 수가 있는데요.

const Input = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

React 개발자 도구에서 forwardRef() 함수를 사용해도 컴포넌트 이름이 나오게 하는 몇가지 방법을 알려드리겠습니다.

첫 번째 방법은 forwardRef() 함수에 익명 함수를 대신에 이름이 있는 함수를 넘깁니다.

const Input = forwardRef(function Input(props, ref) {
  return <input type="text" ref={ref} />;
});

두 번째 방법은 forwardRef() 함수의 호출 결과로 기존 컴포넌트를 대체합니다.

function Input(props, ref) {
  return <input type="text" ref={ref} />;
}

Input = forwardRef(Input);

마지막 방법은 forwardRef() 함수의 호출 결과의 displayName 속성에 컴포넌트 이름을 설정해줍니다.

const Input = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

Input.displayName = "Input";

위 세가지 방법 중에 본인의 취향에 맞는 방법을 사용하시면 되겠습니다. (저는 개인적으로 두 번째 방법을 선호합니다.)

전체 코드

본 포스팅에서 작성한 코드는 아래에서 직접 확인하고 실행해볼 수 있습니다.

마치면서

이상으로 React의 forwardRef() 함수를 사용하여 어떻게 React 컴포넌트에 ref prop을 넘길 수 있는지에 대해서 알아보았습니다.

일반적으로 forwardRef() 함수는 HTML 엘리먼트 대신에 사용되는 최말단 컴포넌트(ex. <Input/>, <Button/>)를 대상으로 주로 사용되며, 그 보다 상위 컴포넌트에서는 forwardRef() 함수를 사용하는 것이 권장되지 않습니다. 왜냐하면 어떤 컴포넌트의 내부에 있는 HTML 엘리먼트의 레퍼런스를 외부에 있는 다른 컴포넌트에서 접근하도록 하는 것은 컴포넌트 간의 결합도(coupling)을 증가시켜 애플리케이션의 유지보수를 어렵게 만들기 때문입니다.