useRef

Chaerin Kim·2023년 11월 27일

렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook

const ref = useRef(initialValue)

Reference

useRef(initialValue)

컴포넌트의 최상위 레벨에서 useRef를 호출하여 ref를 선언함.

import { useRef } from 'react';

function MyComponent() {
  const intervalRef = useRef(0);
  const inputRef = useRef(null);
  // ...

Parameters

  • initialValue: ref 객체의 current 프로퍼티를 처음에 설정할 값. 모든 type의 값이 허용됨. 이 인수는 초기 렌더링 이후에는 무시됨.

Returns

단일 프로퍼티를 가진 객체를 반환함:

  • current: 처음에는 전달한 initialValue로 설정됨. 나중에 다른 값으로 설정할 수 있음. ref 객체를 JSX 노드에 대한 ref 어트리뷰트로 React에 전달하면 React가 current 프로퍼티를 설정함.

다음 렌더링에서 useRef는 동일한 객체를 반환함.

Caveats

  • state와 달리 ref.current는 프로퍼티를 변경할 수 있음. 하지만 렌더링에 사용되는 객체(예: state의 일부)를 담고 있다면 해당 객체를 변경해서는 안됨.
  • ref.current 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않음. ref는 일반 JavaScript 객체이기 때문에 React는 사용자가 언제 변경했는지 알지 못함.
  • 초기화를 제외하고는 렌더링 중에 ref.current를 쓰거나 읽지 말 것. 이렇게 하면 컴포넌트의 동작을 예측할 수 없게 됨.
  • Strict Mode에서는 실수로 발생한 불순물을 찾기 위해 React가 컴포넌트 함수를 두 번 호출함. 이는 개발 단계 전용 동작이며 프로덕션에는 영향을 미치지 않음. 각 ref 객체는 두 번 생성되지만 두 버전 중 하나는 버려짐. 컴포넌트 함수가 순수하다면(그래야 함) 동작에 영향을 미치지 않음.

Usage

Referencing a value with a ref

컴포넌트의 최상위 레벨에서 useRef를 호출하여 하나 이상의 ref를 선언:

import { useRef } from 'react';

function Stopwatch() {
  const intervalRef = useRef(0);
  
  // ...

useRef는 처음에 제공한 초기 값으로 설정된 단일 current 속성이 있는 ref 객체를 반환함.

다음 렌더링에서 useRef는 동일한 객체를 반환함. current 속성을 변경하여 정보를 저장하고 나중에 읽을 수 있음. 이는 state를 떠올리게 할 수 있지만 중요한 차이점이 있음.

Ref를 변경해도 재렌더링이 트리거되지 않음. 즉, ref는 컴포넌트의 시각적 결과에 영향을 주지 않는 정보를 저장하는 데 적합함. 예를 들어 interval ID를 저장했다가 나중에 검색해야 하는 경우, 이를 ref에 넣으면 됨. Ref 내부의 값을 업데이트하려면 current 속성을 수동으로 변경해야함:

function handleStartClick() {
  const intervalId = setInterval(() => {
    // ...
  }, 1000);
  intervalRef.current = intervalId;
}

나중에 ref으로부터 해당 invertal ID를 읽어서 해당 interval을 지울 수 있음:

function handleStopClick() {
  const intervalId = intervalRef.current;
  clearInterval(intervalId);
}

Ref를 사용하면 다음을 보장할 수 있음:

  • 렌더링할 때마다 재설정되는 일반 변수와 달리, 리렌더링 사이에 정보를 저장할 수 있음.
  • 리렌더링을 트리거하는 state 변수와 달리, 변경해도 리렌더링이 트리거되지 않음.
  • 공유되는 외부 변수와 달리, 컴포넌트의 각 복사본에 로컬로 저장됨.

Ref를 변경해도 다시 렌더링이 트리거되지 않으므로 화면에 표시하려는 정보를 저장하는 데는 ref가 적합하지 않음. 대신 state를 사용할 것.

참고: useRefuseState 중 하나를 선택하는 방법

Pitfall

렌더링 중에는 ref.current를 쓰거나 읽지 말 것.

React는 컴포넌트의 본문이 순수한 함수처럼 동작할 것으로 기대함:

  • 입력(props, state, context)이 동일하다면 정확히 동일한 JSX를 반환해야함.
  • 다른 순서나 다른 인수로 컴포넌트를 호출해도 결과에 영향을 미치지 않아야함.

렌더링 중에 참조를 읽거나 쓰면 이러한 기대치가 깨짐.

function MyComponent() {
  // ...

  // 🚩 Don't write a ref during rendering
  myRef.current = 123;

  // ...

  // 🚩 Don't read a ref during rendering
  return <h1>{myOtherRef.current}</h1>;
}

대신 이벤트 핸들러나 Effect에서 참조를 읽거나 쓸 수 있음.

function MyComponent() {

  // ...

  useEffect(() => {
    // ✅ You can read or write refs in effects
    myRef.current = 123;
  });

  // ...

  function handleClick() {
    // ✅ You can read or write refs in event handlers
    doSomething(myOtherRef.current);
  }

  // ...

}

렌더링 중에 무언가를 읽거나 써야 하는 경우 state를 대신 사용할 것.

이러한 규칙을 어겨도 컴포넌트는 여전히 작동할 수 있지만, React에 추가되는 대부분의 새로운 기능은 이러한 기대치에 의존할 것.

참고: 컴포넌트를 순수하게 유지하는 방법

Manipulating the DOM with a ref

Ref를 사용하여 DOM을 조작하는 것은 매우 일반적이고, React는 이를 기본적으로 지원함.

먼저, 초기값이 null인 ref 객체를 선언:

import { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);
  
  // ...

그런 다음, 조작하려는 DOM 노드의 JSX에 ref 객체를 ref attribute로 전달:

  // ...

  return <input ref={inputRef} />;

React가 DOM 노드를 생성하고 화면에 배치하면 React는 ref 객체의 current 프로퍼티를 해당 DOM 노드로 설정함. 이제 <input>의 DOM 노드에 접근하여 focus()와 같은 메서드를 호출할 수 있음:

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

React는 노드가 화면에서 제거되면 current 프로퍼티를 null로 다시 설정함.

참고: Ref로 DOM을 조작하는 방법

Avoiding recreating the ref contents

React는 초기 ref 값을 한 번 저장하고 다음 렌더링에서 이를 무시함.

function Video() {
  const playerRef = useRef(new VideoPlayer());
  
  // ...

new VideoPlayer()의 결과는 초기 렌더링에만 사용되지만, 여전히 모든 렌더링에서 이 함수를 호출하게 됨. 값비싼 객체를 생성하는 경우, 낭비가 될 수 있음.

이 문제를 해결하려면 다음과 같이 ref를 초기화할 수 있음:

function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
  
  // ...

일반적으로 렌더링 중에 ref.current를 쓰거나 읽는 것은 허용되지 않음. 하지만 이 경우에는 결과가 항상 동일하고 초기화 중에만 조건이 실행되므로 충분히 예측 가능하기 때문에 괜찮음.

0개의 댓글