React 공부할끄니까! 렌더링에 필요하지 않은 값은 useRef로 관리?!

React 공부할끄니까! 렌더링에 필요하지 않은 값은 useRef로 관리?!

·

4 min read

Overview.

  • 렌더링에 필요하지 않은 값 참조
  • 따라서 값이 변경되도 컴포넌트 리렌더링은 발생하지 않음
  • DOM 요소 참조하거나 렌더링 영향이 없는 변수를 선언할 때 씀
    • 스타일 변경하기, 포커싱 하기 등의 DOM 컨트롤

Shape.

import { useRef } from 'react';

function MyComponent() { 
    const a = useRef(0);
    const b = useRef(null);
}

매개변수

  • useRef는 매개변수로 initialValue를 받을 수 있음
  • 이는 current 프로퍼티 초기 설정값임
  • 어떤 유형의 값도 올 수 있음
  • 초기 렌더링 이후부터 무시됨

반환값

  • 단일 프로퍼티를 가진 ref 객체를 반환함
  • initialValue로 설정된 current를 바꾸거나, ref 객체를 current 프로퍼티로 설정함

주의

  • ref.current 프로퍼티는 state와 달리 변이할 수 있음 -> mutable
  • 그러나 렌더링에 사용되는 객체를 포함하는 경우 해당 객체를 변이해선 안됨
  • ref.current 프로퍼티를 변경해도 컴포넌트는 리렌더링하지 않음
  • ref는 일반 자바스크립트 객체 -> React는 사용자가 언제 변경했는지 모름
  • 초기화를 제외하고 렌더링 중에 ref.current를 쓰거나 읽어선 안됨

주의에 대한 자세한 설명

  • React 렌더링 원칙과 관련 있음
  • 예측 가능성과 일관성
    • React 렌더링은 순수해야 함
    • 즉, 같은 propsstate에 대해 항상 같은 JSX를 반환해야 함
    • 렌더링 중에 참조하는 객체를 직접 변이하면 이 순수성이 깨짐
  • 의도하지 않은 부작용

    function BadComponent() { 
      const objRef = useRef({ count: 0 })
    
      if(objRef.current.count < 5) { 
          objRef.current.count += 1  //  렌더링 중 변이
      }
    
      return <>{objRef.current.count}</>
    }
    
  • 위 경우에 렌더링할 때 마다 count 증가
  • 때문에 개발 모드에서 두 번 렌더링 됨 -> 예상치 못한 결과
  • 다른 컴포넌트가 같은 객체를 참조하면다면 -> 예측 불가능성
function GoodComponent() { 
    const objRef = useRef({ count: 0 })

    const handleClick = () => { 
        // 이벤트 핸들러에서 변이하도록 함
        objRef.current.count += 1;
        // 필요에 따라 setState로 리렌더링
    }

    return <button onClick={handleClick}>{objRef.current.count}</button>
}
  • ref는 DOM 노드나 타이머 ID와 같은 렌더링과 무관한 값을 저장해야 함
  • 렌더링에 영향을 주는 데이터는 state로 관리해야 함

Usage.

import { useState, useRef } from 'react';

export default function Stopwatch() {
  // 스톱워치 시작 시간과 현재 시간 상태 관리
  const [startTime, setStartTime] = useState(null);
  // 현재 시간을 계속 업데이트하며 저장
  const [now, setNow] = useState(null);
  // setInterval ID를 저장하기 위한 ref
  // 렌더링과 무관하고, 리렌더링되어도 유지됨
  const intervalRef = useRef(null);

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

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now()); // 10ms마다 현재 시간 업데이트
    }, 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로 DOM 조작하기

  • 우선 initialValuenullref 객체 선언
function MyComponent() { 
    const inputRef = useRef(null)
}
  • 그 다음 ref 객체를 ref 어트리뷰트로 조작하려는 DOM 노드의 JSX에 전달
return <input ref={inputRef} />
  • 리액트가 DOM 노드를 생성하고 화면에 그리면, ref 객체의 current 프로퍼티를 DOM 노드로 설정함
  • DOM 노드는 <input>에 접근해 focus() 같은 메서드를 호출할 수 있게됨
function handleClick() { 
    inputRef.current.focus()
}
  • 노드가 화면에서 제거되면 currentnull이 됨

ref 콘텐츠 재생성 피하기

function Video() { 
    const playerRef = useRef(new VidoePlayer())
}
  • 위와 같은 코드는 컴포넌트가 리렌더링될 때 마다 비디오 플레이어 객체를 새로 생성해 ref 객체에 할당함 -> 낭비 발생
function Video() { 
    const playerRef = useRef(null);
    if(playerRef.current === null) { 
        playerRef.current = new VideoPlayer();
    }
}
  • 이 경우에는 결과가 항상 동일하고 초기화 중에만 조건이 실행됨 -> 충분히 예측 가능

커스텀 컴포넌트에 대한 ref 얻기

const inputRef = useRef(null)
return <MyInput ref={inputRef} />
  • 컴포넌트에 ref를 전달하려고 하면 에러 발생
⛔️ 경고: 함수 컴포넌트에는 ref를 지정할 수 없습니다. 이 ref에 접근하려는 시도는 실패합니다. React.forwardRef()를 사용하려고 하셨나요?
  • 기본적으로 컴포넌트는 내부 DOM 노드에 대한 ref를 외부로 노출하지 않음
  • 이 문제를 해결하려면 forwardRefref를 가져오고자 하는 컴포넌트를 감싸야 함
  • 그러면 부모 컴포넌트에서 ref를 가져올 수 있음
const MyInput = forwardRef(({ value, onChange }, ref) => { 
    return ( 
        <input
            value={value}
            onChange={onChange}
            ref={ref}
        />
    )
})

export default MyInput
  • 아래는 이해를 돕는 예제
import { useRef, forwardRef } from 'react';

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

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

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

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

Results.

  • 렌더링에 필요하지 않은 값은 useRef로 관리할 수 있음
  • 렌더링 중에 ref.current 프로퍼티를 읽거나 쓰면 안됨
    • 렌더링에 영향을 주는 데이터는 state로 관리할 것
  • 주로 DOM을 조작하는데 쓰임
  • 부모 컴포넌트가 자식 컴포넌트를 ref로 사용하려면 자식 컴포넌트는 forwardRef로 감싸야 함

References.