FrontEnd/React.js

React Ref 이해하기

SambaLim 2021. 9. 29. 23:58

일반적으로 React에서는 부모 컴포넌트와 자식 컴포넌트가 상호작용할 수 있는 수단이 props 밖에 없습니다. 자식을 수정하려면 새로운 props 를 전달하여 자식을 다시 렌더링해야합니다.

하지만 React에서의 상태의 흐름에서 벗어나 직접적으로 자식 컴포넌트의 인스턴스 혹은 DOM 엘리먼트를 수정해야하는 경우도 있습니다.

VanillaJS

일반적으로 JS에서 DOM Element를 수정해야하는 경우, 특정 DOM을 지정하기 위해 선택자를 사용합니다. 특정한 DOM요소에 style을 따로 적용하거나 속성 값을 바꾸는등 다양한 작업을 할 수 있습니다.

/* <div id="my-id">안녕하세요</div> */

// 예시
const myElement = document.querySelector('#my-id');
myElement.style.color = 'red';

VanillaJS와 달리 React에서는 특정 DOM을 직접적으로 수정해야하는 경우, Ref를 사용합니다.

React에서 Ref를 사용하는 경우

VanillaJS와 달리 React에서는 특정 DOM을 직접적으로 수정해야하는 경우, Ref를 사용합니다.

  • 특정 element에 포커스, 속성값을 관리할 때.
  • 특정 element에 애니메이션을 직접적으로 실행시킬 때.
  • 서드 파티 DOM 라이브러리를 React와 같이 사용할 때.

주의사항

선언적으로 해결될 수 있는 문제에 대해서는 ref 사용을 지양해야합니다.

예를들어 Dialog 컴포넌트를 만들 때 open() , close() 를 두는 대신 isOpen 이라는 props 를 전달하여 만들도록 합니다.

ref 를 사용하기 전에 어느 컴포넌트 계층에서 상태를 소유해야하는지 고민이 필요합니다. 대부분의 경우, 상태를 소유해야하는 적절한 장소가 더 높은 컴포넌트라는 결론이 날 것입니다.

useRef

함수형 컴포넌트에서 ref 를 사용할때는 useRef 라는 Hook API를 사용합니다.

const refContainer = useRef(initialValue);

useRef.current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환합니다.

예시

function CustomTextInput(props) {
  // textInput은 ref 어트리뷰트를 통해 전달되기 위해서
  // 이곳에서 정의되어야만 합니다.
  const textInput = useRef(null);

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

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

Forwarding Refs

보기드문 경우지만, 부모 컴포넌트에서 자식 컴포넌트의 DOM요소에 접근해야하는 경우도 있습니다. 컴포넌트의 DOM요소에 접근하는 것은 컴포넌트의 캡슐화를 파괴하기 때문에 권장하지는 않습니다. 하지만 재사용 가능한 컴포넌트 라이브러리와 같이 특정 컴포넌트에서는 유용할 수 있습니다.

React Component에서 ref를 prop으로 사용해보기

forwardRef 를 어느 경우에 사용하는지 보기 위해 refprop으로 넘겨보는 예시를 만들어봅니다.

import React, { useRef } from "react";

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

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

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

  return (
    <>
      <Input ref={inputRef} />
      <button onClick={handleButton}>Focus Input Elemnt</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://5drl6.csb.app/src/App.tsx:18:3)
    at App (https://5drl6.csb.app/src/App.tsx:31:38)

Warning: Function components cannot be given refs.
Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `App`.
    at Input (https://5drl6.csb.app/src/App.tsx:18:3)
    at App (https://5drl6.csb.app/src/App.tsx:31:38)

'refprop 으로 전달할 수 없어 undefined 값으로 반환될 것이다.' 라는 오류메시지와 '함수형 컴포넌트는 ref 를 줄 수 없기에 forwardRef 를 사용할 의도가 있었냐'는 메시지가 나옵니다.

이러한 오류는 간단하게 ref 로 지정했던 prop 의 이름을 mRef 와 같이 다른 이름으로 변경하는 것으로도 해결할 수 있습니다. 하지만 분명하지 않은 이름과 새로운 컴포넌트 디자인은 컴포넌트를 혼란스럽게 합니다.

forwardRef 사용해보기

React 컴포넌트에서 ref 를 사용하기 위해서는 forwardRef 를 사용해야합니다. forwardRef 를 사용하여 더 아래의 컴포넌트에 ref 를 전달할 수 있게됩니다.

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

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

Input = forwardRef(Input);

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

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

  return (
    <>
      <Input ref={inputRef} />
      <button onClick={handleButton}>Focus Input Elemnt</button>
    </>
  );
}

Input 컴포넌트에 forwardRef 를 적용하여 <input> DOM 노드에 대한 참조를 가져올 수 있고, 필요한 경우 <input> DOM에 직접 접근하여 사용할 수 있습니다.

위의 예시에서 일어나는 단계를 순서대로 보면 다음과 같습니다.

  1. useRef Hook API로 생성한 React refinputRef 변수에 할당합니다.
  2. ref 를 JSX 속성으로 지정하여 <Input ref={inputRef} /> 와 같이 전달합니다.
  3. React는 refforwardRef 내부의 function Input(props, ref) 함수의 두 번째 인자로 전달합니다.
  4. 함수의 두 번째 인자로 전달받은 ref 를 JSX 속성으로 지정하여 <input type="text" ref={ref} /> 와 같이 전달합니다.
  5. ref 가 첨부되면 inputRef.current<input> DOM 노드를 가리키게 됩니다.

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandleref 를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값을 커스터마이즈합니다.

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

function Input(props, ref) {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.value = 3;
      inputRef.current.focus();
    }
  }));

  return <input type="text" ref={inputRef} />;
}

Input = forwardRef(Input);

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

  function handleButton() {
    ref.current.focus();
  }

  return (
    <>
      <Input ref={ref} />
      <button onClick={handleButton}>Focus Input Elemnt</button>
    </>
  );
}

위의 예시에서 <Input ref={ref} /> 를 렌더링한 App 컴포넌트는 ref.current.focus() 를 통해 Input 컴포넌트 내에서 useImperativeHandle 를 사용하여 정의한 focus 를 호출할 수 있습니다.

마무리

디자인 시스템 Select 컴포넌트 작업을 하면서 부모 컴포넌트에서 자식 컴포넌트의 DOM요소에 접근해야하는 케이스가 있었습니다. 따라서 이를 가능하게 하기 위해 React에서 DOM요소를 접근할 수 있게하는 ref 와 부모 컴포넌트에서 자식 컴포넌트의 DOM요소에 접근할 수 있게 하도록 하는 forwardRef 에 대해 공부하고 이를 요약하여 적어보았습니다.

React에서 부모 컴포넌트에서 자식 컴포넌트으로 값을 넘겨 자식 컴포넌트를 다루는 경우가 일반적이 였지만, HTML 엘리먼트 대신에 사용되는 Input, Select와 같은 말단의 특정 컴포넌트에서 사용됩니다. 이보다 상위의 컴포넌트에서는 컴포넌트의 DOM요소에 접근하는 것은 Component의 캡슐화를 방해햐기 때문에 지양해야합니다.

참고문서

'FrontEnd > React.js' 카테고리의 다른 글

React Hook-flow 이해하기  (0) 2021.11.26
useEffect 이해하기  (1) 2021.10.28
React 에서 key값을 index로 하면 안되는 이유  (2) 2021.07.29
상태 관리에 대해 정리해보자  (0) 2020.12.30
React Hooks 알아보기  (1) 2020.03.10