본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -Manipulating the DOM with Refs(ref로 DOM 조작하기)

React는 렌더링 출력과 일치하도록 DOM을 자동으로 업데이트하므로 컴포넌트가 자주 조작할 필요가 없지만, 노드에 초첨을 맞추거나 스크롤하거나 크기위 위치를 측정하기 위해 React가 관리하는 DOM 요소에 접근해야 하는 경우가 있는데, 이때 ref를 필요하다.

Getting a ref to the node(노드에 대한 ref 가져오기)

import { useRef } from 'react';

const myRef = useRef(null);

<div ref={myRef}>

myRef.current.scrollIntoView();

- 컴포넌트 내부에서 ref를 선언하고, DOM 노드를 가져올 JSX 태그에 ref 속성으로 참조를 전달.

- React는 이 노드에 대한 참조를 myRef.current에 넣고, 이벤트 핸들러에서 DOM노드에 엑세스하고 브라우저 API 사용.


Example: Scrolling to an element(예: element로 스크롤하기)

- 컴포넌트는 ref를 하나 이상 포함할 수도 있다.

- 아래 예제는, 세 개의 이미지로 구성된 캐러셀에서 각 버튼을 눌렀을 때, DOM 노드에서 브라우저 scrollIntoView() 메서드를 호출하여 해당 이미지를 중앙에 배치한다.

import { useRef } from 'react';

export default function CatFriends() {
  const firstCatRef = useRef(null);
  const secondCatRef = useRef(null);
  const thirdCatRef = useRef(null);

  function handleScrollToFirstCat() {
    firstCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToSecondCat() {
    secondCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToThirdCat() {
    thirdCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  return (
    <>
      <nav>
        <button onClick={handleScrollToFirstCat}>
          Tom
        </button>
        <button onClick={handleScrollToSecondCat}>
          Maru
        </button>
        <button onClick={handleScrollToThirdCat}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          <li>
            <img
              src="https://placekitten.com/g/200/200"
              alt="Tom"
              ref={firstCatRef}
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/300/200"
              alt="Maru"
              ref={secondCatRef}
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/250/200"
              alt="Jellylorum"
              ref={thirdCatRef}
            />
          </li>
        </ul>
      </div>
    </>
  );
}

- scrollIntoView() : https://developer.mozilla.org/ko/docs/Web/API/Element/scrollIntoView

 

* ref 콜백을 사용하여 refs 목록을 관리하는 방법

- 예를 들어, 목록의 각 항목에 대해 얼마나 많은 ref가 필요할지 알 수 없는 경우가 있다.

- 훅은 컴포넌트의 최상위 레벨에서만 호출해야 하기 때문에, 반복문 또는 map() 내부에서 useRef를 호출할 수는 없다.

- 이런 경우, 부모 앨리먼트에 대한 단일 ref를 가져온 다음, querySelectorAll 과 같은 DOM 조작 메서드를 사용하여 개별 하위 노드를 "찾는" 방법이 있지만, DOM 구조가 변경되는 경우 문제가 될 수 있다.

- 다른 방법으로는, ref 속성에 함수(ref 콜백)를 전달하는 것이다. 

- 아래 예제에서, React는 ref를 설정할 때가 되면 DOM 노드로, 지울 때가 되면 null로 ref 콜백을 호출할 것이다. 이를 통해 자신만의 배열이나 Map을 유지 관리하고 인덱스나 일종의 ID로 모든 ref에 접근할 수 있다.

- 아래 예제에서 itemRef는 단일 노드를 보유하는 것이 아닌, DOM 노드로의 Map을 보유한다.(Ref는 모든 값을 보유할 수 있다.)

import { useRef } from 'react';

export default function CatFriends() {
  const itemsRef = useRef(null);

  function scrollToId(itemId) {
    const map = getMap();
    const node = map.get(itemId);
    node.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // Initialize the Map on first usage.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToId(0)}>
          Tom
        </button>
        <button onClick={() => scrollToId(5)}>
          Maru
        </button>
        <button onClick={() => scrollToId(9)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          {catList.map(cat => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat.id, node);
                } else {
                  map.delete(cat.id);
                }
              }}
            >
              <img
                src={cat.imageUrl}
                alt={'Cat #' + cat.id}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i
  });
}

Accessing another component's DOM nodes(다른 컴포넌트의 DOM 노드에 접근하기)

- 빌트인 컴포넌트와 달리 커스텀 컴포넌트의 경우에 ref를 넣으려고 하면 기본적으로 null이 반환된다.

- 이는 React가 컴포넌트가 다른 컴포넌트(자식 컴포넌트 포함)의 DOM 노드에 접근하는 것을 허용하지 않기 때문이다.

- 대신 아래 예제와 같이, 컴포넌트는 자신의 ref를 자식 중 하나에 전달하도록 지정할 수 있다.(forwardRef API 사용)

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

- MyInput 컴포넌트를 forwardRef를 사용하여 선언하면, props 다음의 두 번째 ref 인수에 전달받은 ref를 받도록 설정할 수 있다.

 

* 명령형 핸들로 API의 하위 집합 노출하기

- 위의 예시와 달리, 아래 예시에서는 MyInput은 useImperactiveHandle을 사용해서 원본 DOM input 엘리먼트를 노출하지 않는다.

- MyInput 내부의 realInputRef는 실제 input DOM 노드를 보유하지만, useImperactiveHandle은 부모 컴포넌트에 대한 ref 값으로 고유한 특수 객체를 제공한다. 즉, Form 컴포넌트 내부의 inputRef.current에는 focus 메서드만 있게 된다.

- 이때, ref "핸들"은 DOM 노드가 아닌, useImperactiveHandle() 내부에서 생성한 사용자 정의 객체다.

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

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Only expose focus and nothing else
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});

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

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

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

When React attaches the refs(React가 ref를 첨부할 때)

- 첫 번째 렌더링 중에는 DOM 노드가 아직 생성되지 않았으므로 ref.current는 null이다. 

- React가 커밋하는 동안에 ref.current를 설정한다. 즉, DOM이 업데이트된 직후 해당 DOM 노드로 다시 설정한다.

- 일반적으로 이벤트 핸들러에서 ref에 접근하는데, 특정 이벤트가 없다면, Effect가 필요할 수도 있다.

 

* 플러싱 state는 flushSync와 동기식으로 업데이트됩니다.

- React에서는 state 업데이트가 큐에 등록이 되기 때문에, DOM을 즉시 업데이트하지 않는다.

- 따라서, React가 react-dom의 flushSync 사용해서 DOM을 동기적으로 업데이트("플러시")하도록 강제할 수 있다.

// flushSync 사용전
// state가 업데이트 되기전에 scrollIntoView()가 실행된다
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();


// flushSync 사용
// flushSync로 감싼 코드가 실행된 직후 React가 DOM을 동기적으로 업데이트하도록 지시한다.
flushSync(() => {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

Best practices for DOM manipulation with refs(ref를 이용한 DOM 조작 모범 사례)

- ref를 사용해서 DOM을 수동으로 수정하려고 하면 React가 수행하는 변경 사항과 충돌할 위험이 있다.

- 따라서, React가 관리하는 DOM 노드를 변경하면 안된다.(수정하거나, 자식을 추가하거나 제거하는 경우)

- 다만, React가 업데이트할 이유가 없는 DOM의 일부는 안전하게 수정할 수 있다.

(예를들어, 일부 <div>가 항상 비어 있는 경우, React는 그 자식 목록을 건드릴 이유가 없기 때문에, 수동으로 요소를 추가하거나 제거하더라도 안전하다)


* 참고 : React 공식문서(https://react-ko.dev/learn)