React - useRef, useImperativeHandle Hook

이소라·2022년 9월 13일
0

React

목록 보기
15/23

useRef

const refContainer = useRef(initialValue);
  • useRef.current 프로퍼티가 인수로 전달받은 initialValue로 초기화된 변형 가능한 ref 객체를 반환함
    • 반환된 ref 객체는 컴포넌트의 생명주기 전체에 걸쳐서 유지됨
    • useRef는 보통 자식에 명령적으로 접근하는데 사용함
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current`는 마운트된 text input element를 가리킴
    inputEl.current.focus();
  }
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

ref vs useRef

  • ref는 DOM을 접근하기 위해서 사용됨

    • <div ref={myRef} />를 사용하여 React에 ref 객체를 전달할 경우, React는 그 노드가 변할 때마다 대응되는 DOM 노드를 .current 프로퍼티로 설정함
  • useRef는 클래스의 인스턴스 필드와 비슷하게 가변값을 유지하는 데 유용함

    • useRef가 순수 JavaScript 객체를 생성하기 때문임
    • useRef는 매번 렌더링 할 때 동일한 ref 객체를 제공함
function Timer() {
  const intervalRef = useRef();
  
  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    // effect 내부에서 interval을 취소할 경우, useRef를 사용하지 않아도 됨
    return () => {
      clearInterval(intervalRef.current);
    };
  });
  // 이벤트 핸들러에서 interval을 취소할 경우, useRef를 사용해서 취소할 interval을 기억해놓을 수 있음 
  function handleCancleClick() {
    clearInterval(intervalRef.current);
  }
  return (
    <>
      <h1>Timer</h1>
      <button onClick={handleCancleClick}>Cancle</button>
    </>
  );
}
  • useRef.current 프로퍼티가 변형되도 리렌더링이 발생하지 않음
    • React가 DOM 노드에 ref를 붙이거나 뗄 때 어떤 코드를 실행시키고 싶다면, callback ref를 대신 사용해야 함

callback ref

  • DOM 노드의 위치나 크기를 측정하기 위해 기본적으로 callbak ref를 사용함
    • React는 ref가 다른 노드에 붙을 때 마다 해당 콜백 함수를 실행시킴
    • callback refuseRef와 달리 ref 내용이 변경될 경우 부모 컴포넌트에게 알려주기 때문에, 측정값이 갱신됨
function MeasureExample() {
  const [height, setHeight] = useState(0);
  
  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);
  
  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}
  • useCallback의 두 번째 인수 dependency 배열로 []을 전달할 경우, ref callback이 리렌더링 사이에 변하지 않는다는 것을 보장함

    • 그래서 React가 ref callback을 불필요하게 호출하지 않음
  • 위 코드에서 callback ref는 컴포넌트가 마운트되고 해제될때만 호출됨

    • 컴포넌트의 크기가 변할 때마다 알림을 받고 싶다면, ResizeObserver를 사용하거나 관련된 3th party Hook을 사용해야 함
      • ResizeObserver : element의 content 또는 border box나 SVGElement의 bounding box의 크기 변화를 알려주는 interface (예시)
  • 위 코드의 로직을 재활용 가능한 Hook으로 추출할 수 있음

function MeasureExample() {
  const [rect, ref] = useClientRect();
  
  return (
    <>
      <h1 ref={ref}>Hello, world</h1>
      {rect !== null &&
        <h2>The above header is {Math.round(rect.height)} px tall</h2>
      }
    </>
  );
}

function useClientRect() {
  const [rect, setRect] = useState(null);
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect());
    }
  }, []);
  return [rect, ref];
}

lazy initialized useRef

  • useRefuseState처럼 함수 오버로드를 받아들이지 않으므로, 초기값을 지연 세팅해주는 함수를 만들어서 사용해야 함
// bad
function Image(props) {
  // 매번 렌더링할 때마다 IntersectionObserver가 생성됨
  const ref = useRef(new IntersectionObserver(onIntersect));
}

// good
function Image(props) {
  const ref = useRef(null);
  
  // IntersectionObserver는 한번만 지연적으로 생성됨
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }
  
  // ref.current에 접근 해야할 때 getObserver()를 호출함
}



useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])
  • useImperativeHandleref를 사용할 때 부모 컴포넌트에 노출된 인스턴스 값을 커스터마이징할 수 있음
    • useImperativeHandleforwardRef와 같이 사용해야 함
      • useImerativeHandle의 첫 번째 인자 : 프로퍼티를 부여할 ref
      • useImerativeHandle의 두 번째 인자 : ref 객체에 추가하고 싶은 프로퍼티를 정의함
function FancyInput(props, ref) {
  const inputRef = useRef();
  useImpertiveHandle(ref, () => {
    reallyFocus: () => {
      inputRef.current.focus();
      console.log('Being focused!');
    }
  }));
  return <input ref={inputRef} ... />;
}

FancyInput = forwardRef(FancyInput);
  • 위 코드에서 <input ref={inputRef} />를 렌더링하는 부모 컴포넌트 FancyInput는 inputRef.current.reallyFocus를 호출할 수 있음

  • useImperativeHandle을 사용할 때의 장점

    • 자식 컴포넌트의 상태나 로직을 isolate할 수 있음
    • 상태를 끌어올리지 않고 부모 컴포넌트에 변경된 데이터를 적용할 수 있음
      • 상태나 로직은 자식 컴포넌트가 갖고 있고, 부모 컴포넌트는 ref.current에서 필요한 프로퍼티를 가져오기만 하면 됨

ref를 사용한 명령형 코드를 자제해야 하는 이유

  • 캡슐화를 깨뜨림
    • React에서는 컴포넌트들끼리 props를 통해서만 의사 소통하는 것이 요구되어지는데, ref를 사용할 경우 다른 컴포넌드틀과 props가 아닌 ref attribute 사용해서 의사 소통할 수 있음
  • React의 data-driven pattern을 따르지 않음
    • ref는 데이터 동기화를 보장하지 않으므로, 애플리케이션의 data-driven action을 잃을 것임
      • state가 갱신되지 않음
      • 컴포넌트가 리렌더링되지 않음
      • 애플리케이션의 state가 DOM의 변화를 추적하지 못함
    • event-driven 개발을 권장됨
      • ref를 사용할 경우, 데이터 변화에 따라서가 아니라 이벤트(사용자 click 등)에 따라서 애플리케이션 UI가 갱신되는 것이 권장됨



참고

0개의 댓글