Hooks - useRef

흑우·2023년 8월 9일

useRef(initialValue)

  • useRef는 렌더링에 필요하지 않은 값을 참조할 수 있는 React 훅입니다.

참조

선언하기

  • 컴포넌트의 최상위 레벨에서 useRef를 호출하여 ref를 선언합니다.
function MyComponent() {
  const intervalRef = useRef(0);
  const inputRef = useRef(null);
  // ...

매개변수

  • initialValue : ref 객체의 current 프로퍼티 초기 설정값입니다. 여기에는 어떤 유형의 값이든 지정할 수 있고, 초기 렌더링 이후부터는 무시됩니다.

반환값

  • useRef는 단일 프로퍼티를 가진 객체를 반환합니다.
    • current: 처음에는 전달한 initialValue로 설정됩니다. 나중에 다른 값으로 바꿀 수 있습니다.
    • ref 객체를 JSX 노드의 ref 속성으로 React에 전달하면 React는 current 프로퍼티를 설정합니다.
  • 다음 렌더링에서 useRef는 동일한 객체를 반환합니다.

주의사항

  • ref.current 프로퍼티는 state와 달리 변이할 수 있습니다. 그러나 렌더링에 사용되는 객체(state)를 포함하는 경우 해당 객체를 변이해서는 안됩 니다.
  • ref.current 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않습니다. ref는 일반 JavaScript 객체이기 때문에 React는 사용자가 언제 변경했는지 알지 못합니다.
  • 초기화를 제외하고는 렌더링 중에 ref.current를 쓰거나 읽지마세요. 이렇게 하면 컴포넌트의 동작을 예측할 수 없게 됩니다.
  • Strict Mode에서 React는 컴포넌트 함수를 두 번 호출하여 의도하지 않은 불순물을 찾을 수 있도록 돕습니다. 각 ref 객체는 두 번 생성되고 그 중 하나는 버려집니다.

사용법

😀 ref로 값 참조하기

  • 컴포넌트의 최상위 레벨에서 useRef를 호출하여 하나 이상의 ref를 선언합니다.
function Stopwatch() {
  const intervalRef = useRef(0);
  // ...
  • useRef는 처음에 제공한 초기값으로 설정된 단일 current 프로퍼티가 있는 ref 객체를 반환합니다.
  • 다음 렌더링에서 useRef는 동일한 객체를 반환합니다. 정보를 저장하고 읽을 수 있도록 current 속성을 변경할 수 있습니다.
  • ref를 변경해도 리렌더링을 촉발하지 않습니다. 즉, ref는 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는 데 적합합니다.
    • 예) intervalID를 저장 했다가 나중에 불러와여 하는 경우 ref에 넣을 수 있습니다.
    • ref 내부의 값을 업데이트 하는 경우 current 프로퍼티를 수동으로 변경해야합니다.
    • 나중에 ref에서 해당 interval ID를 읽어 해당 interval을 취소할 수 있습니다
function handleStartClick() {
  const intervalId = setInterval(() => {
    // ...
  }, 1000);
  intervalRef.current = intervalId;
}

function handleStopClick() {
  const intervalId = intervalRef.current;
  clearInterval(intervalId);
}
  • ref를 사용하면 다음을 보장합니다:
    • (렌더링할 때마다 재설정되는 일반 변수와 달리) 리렌더링 사이에 정보를 저장할 수 있습니다.
    • (리렌더링을 촉발하는 state 변수와 달리) 변경해도 리렌더링을 촉발하지 않습니다.
    • (정보가 공유되는 외부 변수와 달리) 각각의 컴포넌트에 로컬로 저장됩니다.

ref - data 사용 예시

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
      <button onClick={handleStop}>
        Stop
      </button>
    </>
  );
}

주의!

  • 렌더링 중에는 ref.current를 쓰거나 읽지 마세요.
  • React는 컴포넌트의 본문이 순수 함수처럼 동작하기를 기대합니다
    • 입력값들(props, state, context)이 동일하면 완전히 동일한 JSX를 반환해야 합니다.
    • 다른 순서나 다른 인수를 사용하여 호출해도 다른 호출의 결과에 영향을 미치지 않아야 합니다.
  • 렌더링 중에 ref를 읽거나 쓰면 이러한 기대가 깨집니다.
function MyComponent() {
  // 🚩 렌더링 중에 ref를 작성하지 마세요.
  myRef.current = 123;
  // 🚩 렌더링 중에 ref를 읽지 마세요.
  return <h1>{myOtherRef.current}</h1>;
}
function MyComponent() {
  // ...
  useEffect(() => {
    // ✅ Effect에서 ref를 읽거나 쓸 수 있습니다.
    myRef.current = 123;
  });
  // ...
  function handleClick() {
    // ✅ 이벤트 핸들러에서 ref를 읽거나 쓸 수 있습니다.
    doSomething(myOtherRef.current);
  }
  // ...
}
  • 렌더링 중에 써야하는 경우라면 state를 사용하세요.

😀 ref로 DOM 조작하기

  • ref를 사용하여 DOM을 조작할 수 있습니다.
  • 먼저 초기값이 null인 ref 객체를 선언하세요.
  • 그런 다음 ref 객체를 ref 속성으로 조작하려는 DOM 노드의 JSX에 전달하세요.
function MyComponent() {
  const inputRef = useRef(null);
  // ...
  
    // ...
  return <input ref={inputRef} />;
  • React가 DOM 노드를 생성하고 화면에 그린 후, React는 ref 객체의 current프로퍼티를 DOM 노드로 설정합니다. 이제 DOM 노드 input에 접근해 focus()와 같은 메서드를 호출할 수 있습니다.
 function handleClick() {
    inputRef.current.focus();
  }
  • 노드가 화면에서 제거되면 React는 current 프로퍼티를 다시 null로 설정합니다.

ref DOM 조작 예시

  1. 이미지 스크롤하기
  • 이 예제에서는 버튼을 클릭하면 이미지가 스크롤됩니다. 목록 DOM 노드에 대한 ref를 사용한 다음 DOM querySelectorAll API를 호출하여 스크롤하려는 이미지를 찾습니다.
export default function CatFriends() {
  const listRef = useRef(null);

  function scrollToIndex(index) {
    const listNode = listRef.current;
    // This line assumes a particular DOM structure:
    // 다음 코드는 특정 DOM 구조를 가정합니다.
    const imgNode = listNode.querySelectorAll('li > img')[index];
    imgNode.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToIndex(0)}>
          Tom
        </button>
        <button onClick={() => scrollToIndex(1)}>
          Maru
        </button>
        <button onClick={() => scrollToIndex(2)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul ref={listRef}>
          <li>
            <img
              src="https://placekitten.com/g/200/200"
              alt="Tom"
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/300/200"
              alt="Maru"
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/250/200"
              alt="Jellylorum"
            />
          </li>
        </ul>
      </div>
    </>
  );
}
  1. 컴포넌트에 ref 노출하기
  • 때로는 부모 컴포넌트가 컴포넌트 내부의 DOM을 조작할 수 있도록 하고 싶을 때가 있습니다.
  • 예를 들어, MyInput 컴포넌트를 작성하는 중인데, 부모 컴포넌트가 (부모가 접근할 수 없는) MyInput의 input에 초점을 맞출 수 있게 하고 싶을 수 있습니다.
  • useRef로 input을 붙잡고 forwardRef로 이를 부모 컴포넌트에 노출시킬 수 있습니다.
const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

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

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

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

😀 ref로 콘텐츠 재생성 피하기

  • React는 초기에 ref 값을 한 번 저장하고, 다음 렌더링부터는 이를 무시합니다.
  • new VideoPlayer()의 결과는 초기 렌더링에만 사용되지만, 호출 자체는 이후의 모든 렌더링에서도 여전히 계속 이뤄집니다. 이는 값비싼 객체를 생성하는 경우 낭비일 수 있습니다.
function Video() {
  const playerRef = useRef(new VideoPlayer());
  // ...
  • 이 문제를 해결하려면 대신 다음과 같이 ref를 초기화할 수 있습니다
function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
  // ...
  • 일반적으로 렌더링 중에 ref.current를 쓰거나 읽는 것은 허용되지 않습니다. 하지만 이 경우에는 결과가 항상 동일하고 초기화 중에만 조건이 실행되므로 충분히 예측할 수 있으므로 괜찮습니다.

useRef를 초기화할 때 null 검사를 피하는 방법

  • 타입 검사기를 사용하면서 항상 null을 검사하고 싶지 않다면 다음과 같은 패턴을 대신 사용해 볼 수 있습니다
function Video() {
  const playerRef = useRef(null);

  function getPlayer() {
    if (playerRef.current !== null) {
      return playerRef.current;
    }
    const player = new VideoPlayer();
    playerRef.current = player;
    return player;
  }

  // ...
  • 여기서 playerRef 자체는 nullable합니다. 하지만 타입 검사기에 getPlayer()가 null을 반환하는 경우가 없다는 것을 확신시킬 수 있어야 합니다. 그런 다음 이벤트 핸들러에서 getPlayer()를 사용하십시오.

문제 해결

😀 커스텀 컴포너트에 대한 ref를 얻을 수 없습니다.

  • 컴포넌트에 ref를 전달하고자 합니다.
const inputRef = useRef(null);

return <MyInput ref={inputRef} />;
  • 다음과 같은 오류가 발생합니다.
  • 기본적으로 컴포넌트는 내부의 DOM 노드에 대한 ref를 외부로 노출하지 않습니다.
  • 이 문제를 해결하려면 ref를 가져오고자 하는 컴포넌트를 찾으세요.
  • 그런 다음 다음과 같이 forwardRef로 감싸세요.
const MyInput = forwardRef(({ value, onChange }, ref) => {
  return (
    <input
      value={value}
      onChange={onChange}
      ref={ref}
    />
  );
});

export default MyInput;
  • 그러면 부모 컴포넌트의 ref를 가져올 수 있습니다.
profile
흑우 모르는 흑우 없제~

1개의 댓글

comment-user-thumbnail
2023년 8월 9일

좋은 글 잘 읽고 갑니다 ㅎㅎ

답글 달기