React ref와 callback ref에 대해 알아보자

been's devlog·2023년 7월 3일
0

ref란?

react.dev에서의 ref 설명

공식문서에 의하면 다음과 같이 설명되어있다.

React는 렌더링 출력과 일치하도록 DOM을 자동으로 업데이트하므로 구성 요소가 자주 DOM을 조작할 필요가 없습니다. 그러나 경우에 따라 React에서 관리하는 DOM 요소에 액세스해야 할 수도 있습니다. React에는 이러한 작업을 수행하는 기본 제공 방법이 없으므로 DOM 노드에 대한 참조가 필요합니다.

보통 react가 상태변화를 감지하고 정상적인 리렌더링을 수행하기 위해서는 virtual dom의 업데이트를 거쳐 최신 dom과 상태가 컴포넌트에 적용되어야 하는데, 이를 위해서는 state와 props를 갱신하여야 한다.

하지만 사정상 그렇게 할 수 없는 경우, 이러한 리렌더링을 가치는 것이 아닌 DOM 엘리먼트에 직접 접근하도록 react에서 특별히 제공하는 방식이다.

ref의 사용법

eventlistener 등록

원래는 onClick props를 사용하여야 하며 react에서는 권장하지 않는 방법이다. 하지만 사용해야 할 경우가 가끔식 생기므로 예시만 남겨놓았다.

function Button() {
  const buttonRef = useRef(null);

  useEffect(() => {
    const currentElement = buttonRef.current;

    if (currentElement) {
      currentElement.addEventListener('click', () => {
        console.log('Element was clicked!');
      });
    }

    return () => {
      if (currentElement) {
        currentElement.removeEventListener('click', () => {
          console.log('Element was clicked!');
        });
      }
    };
  }, []);

  return <button ref={buttonRef}>Click me!</button>;
}

export default Button;

인스턴스 관리

필자는 거의 대부분 이 경우에 사용한다.
ref는 DOM엘리먼트 뿐만이 아니라 어떠한 값도 자유롭게 저장 가능하다. 만약 react의 렌더링에 직접적으로 영향을 주지 않으면서도 컴포넌트가 활성화되어있는 동안 특정 값을 들고있고 싶다면, ref에 저장하는 것은 좋은 선택이 될 수 있다.
대표적인 예로 IntersectionObserver가 있다.

const useIntersectionObserver = () => {
  // observer는 렌더링에 아무 영향을 받지 않으며 정상적으로 동작 가능하다.
  const observer = useRef<IntersectionObserver>();

  useEffect(() => {
  	observer.current = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
     	// 작업 수행
      });
    });
  }, []);
};

cleanup시 사용

useEffect(() => {},[]) 기준, react의 state는 초기값으로 고정된다. deps가 비어 재생성이 되지 않아 상태 갱신이 안되기 때문인데, 이는 cleanup에서도 마찬가지이며 정리해야 할 작업이 있는경우 유용하게 사용할 수 있다.
주로 하는 작업은 removeEventListener, clearTimeout, clearInterval 등이 있다.

  useEffect(() => {
     return () => {
      if (ref.current) {
        ref.current.removeEventListener('click', () => {
          console.log('Element was clicked!');
        });
      }
    };
  }, []);

ref의 또다른 사용법: callback ref

공식문서의 callback ref
최근에 알게된 또다른 사용방식인데, useRef등을 사용하지 않고 다음과 같이 엘리먼트에서의 ref를 사용가능하다.

const callbackRef = useCallback((node) => {
	// 후속 작업, node에는 div 엘리먼트의 값이 들어오게 된다.
},[])

<div ref={callbackRef} />

다음 코드에서 ref props로 콜백함수를 전달하고 있는데, 여기서 node로는 div엘리먼트의 값이 들어오게 되고, 여기서 기존 ref의 useEffect내에서 수행하는 작업을 실행할 수 있다.
기능적으로 차이는 없지만 ref변수를 별도로 선언하지 않아 더 깔끔하다고 느껴진다.

useCallback 추가는 필수는 아니지만 ref 초기화 작업은 일반적으로 최초렌더링 시점에 수행하므로 불필요한 재생성은 막기위해 추가하는것이 좋다.

ref 사용시 주의점

ui sync 이슈

초반에 ref는 virtual dom을 통한 리렌더링을 거치지 않는다고 하였다. 그렇다는 말은 렌더링과 연관이 있는 값을 ref에 저장하면 싱크가 맞지 않는 등의 문제가 발생할 수 있기 때문에 피해야 한다.

ref props 전달시 주의점

다음과 같이 ref={ref}를 통해 초기화할 경우, 초기화되지 않는 케이스를 주의해야 한다. 예시로 살펴보면 다음과 같다.

const Function = () => {
  const ref = useRef();
	// undefined (최초 실행시)
  console.log('REF1: ', ref.current);

  useEffect(() => {
		// ref-div의 엘리먼트가 저장
    console.log('REF2: 'ref.current);
  }, []);

  return <div ref={ref} className="ref-div" />;
};

REF1이 undefined이고 REF2에서 초기화가 되는 이유는 컴포넌트 렌더링 단계와 연관이 있다.

  1. 컴포넌트 함수가 실행되어 jsx 리턴
  2. jsx를 사용하여 virtual dom 업데이트
  3. virtual dom의 변경내용을 실제 dom에 반영
  4. 완료후 useEffect 실행

여기서 처음으로 REF1을 참조하는 경우, 아직 1단계가 완료되지 않아 실제 DOM에는 ref-div가 아예 존재하지 않는다. 그러니 REF1.current는 undefined로 들어온다.

하지만 REF2의 경우, 4단계까지 완료된 상태이므로 실제 DOM에 존재하며 정상적으로 초기화가 이루어져 엘리먼트의 값이 들어오게 된다.

profile
더 나은 내일의 나를 목표로 하는 프론트엔드 개발자입니다.

0개의 댓글