[리액트 공식문서 읽기] ESCAPE HATCHES - Referencing Values with Refs

JaeHong Jeong·2023년 11월 26일
post-thumbnail

Overview

컴포넌트가 일부 정보를 “기억”하기를 원하지만 해당 정보가 새 렌더링을 트리거하는 것을 원하지 않는 경우 참조를 ref를 사용할 수 있다.

Adding a ref to your component

리액트에서 useRef 훅을 가져와서 컴포넌트에 참조를 추가할 수 있다.

import { useRef } from 'react';

컴포넌트 내에서 useRef 훅을 호출하고 참조하려는 초기 값을 유일한 인수로 전달한다. 예를 들어, 다음은 값 0 에 대한 참조이다.

const ref = useRef(0);

useRef 는 다음과 같은 객체를 반환한다.

{ 
  current: 0 // The value you passed to useRef
}

ref.current 속성을 통해 해당 참조의 현재 값에 액세스할 수 있다. 이 값은 의도적으로 변경가능하다. 즉, 읽고 쓸 수 있다. 이는 리액트가 추적하지 않는 컴포넌트의 비밀 주머니와 같다. (이것이 리액트의 단방향 데이터 흐름에서 “탈출구”를 만드는 이유이다.)

여기서 버튼을 클릭할 때마다 ref.current 를 증가시킨다.

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

참조는 숫자를 가리키지만 상태와 마찬가지로 문자열, 객체, 함수 등 무엇이든 가리킬 수 있다. 상태와 달리 ref는 읽고 수정할 수 이쓴 current 속성이 있는 일반 JavaScript 객체이다.

컴포넌트는 모든 increment마다 리렌더링을 하지않는다. 상태와 마찬가지로 참조는 리렌더링 사이에 리액트에 의해 유지된다. 그러나 상태를 설정하면 컴포넌트가 리렌더링된다. ref는 바꾸는건 리렌더링되지 않는다.

Example: building a stopwatch

단일 컴포넌트에서 참조와 상태를 결합할 수 있다. 예를 들어, 사용자가 버튼을 눌러 시작하거나 중지할 수 있는 스톱워치를 만들어 보겠다. 사용자가 “Start”를 누른 후 얼마나 많은 시간이 지났는지 표시하려면 시작 버튼을 누른 시간과 현재 시간을 추적해야 한다. 이 정보는 렌더링에 사용되므로 다음과 같은 상태로 유지된다.

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

사용자가 “Start”를 누르면 10밀리초마다 시간을 업데이트하기 위해 [setInterval](https://developer.mozilla.org/docs/Web/API/setInterval) 을 사용한다.

import { useState } from 'react';

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

  function handleStart() {
    // Start counting.
    setStartTime(Date.now());
    setNow(Date.now());

    setInterval(() => {
      // Update the current time every 10ms.
      setNow(Date.now());
    }, 10);
  }

  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>
    </>
  );
}

“Stop” 버튼을 누르면 now 상태 변수 업데이트가 중단 되도록 기존 interval를 취소해야 한다. [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval) 을 호출하여 이 작업을 수행할 수 있지만 사용자가 시작을 눌렀을 때 이전에 setInterval 호출에 의해 반환된 interval ID를 제공해야한다. interval ID를 어딘가 보관해야 한다. interval ID는 렌더링에 사용되지 않으므로 참조에 보관할 수 있다.

import { useState, useRef } from 'react';

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를 사용하는 것이 더 효율적일 수 있다.

Differences between refs and state

아마도 너는 ref가 state보다 덜 “엄격”하다고 생각할 것이다. 예를 들어 항상 상태 설정 함수를 사용하는 대신 ref를 변경할 수 있다. 하지만 대부분의 경우 상태를 사용하고 싶을 것이다. Refs는 자주 필요하지 않은 “탈출용 해치”이다. 상태와 참조를 비교하는 방법은 다음과 같다.

refsstate
useRef(initialValue) 는 { current: initialValue } 를 반환한다.useState(initialValue) 는 상태 변수의 현재 값과 상태 설정 함수( [value, setValue] ) 반환
변경할 때 리렌더링되지 않는다.변경하면 리렌더링된다.
변경 가능 -   렌더링 프로세스 외부에서 current 값을 수정하고 업데이트할 수 있다.“불변” - 리렌더링을 대기역에 추가하려면 상태 설정 함수를 사용하여 상태 변수를 수정해야한다.
렌더링하는 동안 current 값을 읽거나 쓰면 안된다.언제든지 상태를 읽을 수 있다. 그러나 각 렌더링에는 변경되지 않는 자체 사태 스냅샷이 있다.

다음은 상태로 구현된 카운터 버튼이다.

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      You clicked {count} times
    </button>
  );
}

count 값이 표시되므로 이에 대한 상태 값을 사용하는 것이 합리적이다. setCount() 를 사용하여 카운터 값을 설정하면 리액트는 컴포넌트를 리렌더링하고 화면은 새 카운트를 반영하도록 업데이트된다.

이를 참조로 구현하려고 하면 리액트는 컴포넌트를 리렌더링하지 않으므로 개수 변경을 볼 수 없다. 이 버튼을 클릭해도 텍스트가 어떻게 업데이트되지 않는지 확인해라.

import { useRef } from 'react';

export default function Counter() {
  let countRef = useRef(0);

  function handleClick() {
    // This doesn't re-render the component!
    countRef.current = countRef.current + 1;
  }

  return (
    <button onClick={handleClick}>
      You clicked {countRef.current} times
    </button>
  );
}

이것이 렌더링 중에 ref.current 를 읽는 것이 신뢰할 수 없는 코드로 이어지는 이유이다. 필요한 경우 대신 상태를 사용해라.

💡 DEEP DIVE

How does useRef work inside?

useStateuseRef 는 모두 리액트에서 제공되지만 원칙적으로 useRefuseState 위에 구현될 수 있다. 리액트 내부에서 useRef 가 다음과 같이 구현되어 있다고 상상할 수 있다.

// Inside of React
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}

첫 번째 렌더링 중에 useRef{ current: initialValue } 를 반환한다. 이 객체는 리액트에 의해 저장되므로 다음 렌더링 중에 동일한 객체가 반환된다. 이 예에서는 상태 설정 함수가 어떻게 사용되지 않는지 확인해라. useRef 는 항상 동일한 객체를 반환해야 하기 때분에 불필요하다.
리액트는 실제로 충분히 일반적이기 때문에 내장 버전의 useRef 를 제공한다. 하지만 setter가 없는 일반 상태 변수로 생각할 수 있다. 객체 지향 프로그래밍에 익숙하다면 refs가 인스턴스 필드를 생각나게 할 수 있다. 하지만 this.something 대신 somethingRef.current 를 작성한다.

When to use refs

일반적으로 컴포넌트가 리액트 외부로 나가서 외부 API( 종종 컴포넌의 모양에 영향을 주지 않는 브라우저 API )와 통실해야 할 때 참조를 사용한다. 다음은 이러한 드문 상황 중 일부이다.

  • timeout ID 저장
  • 다음 페이지에서 다루는 DOM 요소 저장 및 조작
  • JSX를 계산하는 데 필요하지 않은 다른 객체를 저장

컴포넌트가 일부 값을 저장해야 하지만 렌더링 로직에 영향을 주지 않는 경우 refs를 선택해라.

Best practices for refs

다음 원칙을 따르면 컴포넌트를 더욱 예측하기 쉽게 만들 수 있다.

  • refs를 탈출구로 취급해라. Refs는 외부 시스템이나 브라우저 API로 작업할 때 유용하다. 애플리케이션 로직과 데이터 흐름의 대부분이 참조에 의존하는 경우 접근 방식을 다시 생각해 볼 수 있다.
  • 렌더링하는 동안 ref.current 를 읽거나 쓰지 마라. 렌더링 중에 일부 정보가 필요한 경우 상태를 사용해라. 리액트는 ref.current 가 언제 변경되는지 모르기 때문에 렌더링하는 동안 이를 읽어도 컴포넌트의 동작을 예측하기 어렵다. ( 유일한 예외는 첫 번째 렌더링 중에 참조를 한번만 설정 하는 if (!ref.current) ref.current = new Thing() 과 같은 코드이다.

리액트 상태의 제한 사항은 ref에 적용되지 않는다. 예를 들어 상태는 모든 렌더링에 대한 스냅샷처럼 작동하며 동기적으로 업데이트되지 않는다. 그러나 참조의 현재 값을 변경하면 즉시 변경된다.

ref.current = 5;
console.log(ref.current); // 5

이는 참조 자체가 일반 JavaScript 객체이므로 참조 객체처럼 동작하기 때문이다.

또한 ref로 작업할 때 mutation을 피하는 것에 대해 걱정할 필요가 없다. 변경하려는 객체가 렌더링에 사용되지 않는 한 리액트는 참조나 그 내용으로 무엇을 하든 상관하지 않는다.

Refs and the DOM

참조는 어떤 값이든 가리킬 수 있다. 그러나 ref의 가장 일반적인 사용 사례는 DOM 요소를 액세스 하는 것이다. 예를 들어 프로그래밍 방식으로 입력에 집중하려는 경우에 유용하다. <div ref={myRef}> 와 같이 JSX의 ref 속성에 ref를 전달하면 리액트는 해당 DOM 요소를 myRef.current 에 넣는다. 요소가 DOM에서 제거되면 리액트는 myRef.currentnull 로 업데이트한다. 이에 대한 자세한 내용은 Manipulating the DOM with Refs. 에서 읽을 수 있다.

Recap

  • Refs는 렌더링에 사용되지 않는 값을 유지하기 위한 탈출구이다. 자주 필요하지는 않는다.
  • ref는 읽거나 설정할 수 있는 current 라는 단일 속성이 있는 일반 JavaScript 객체이다.
  • useRef 훅을 호출하여 리액트에게 참조를 제공하도록 요청할 수 있다.
  • 상태와 마찬가지로 참조를 사용하면 컴포넌트를 리렌더링하는 사이에 정보를 유지할 수 있다.
  • 상태와 달리 참조의 current 값을 설정해도 리렌더링이 트리거되지 않는다.
  • 렌더링하는 동안 ref.current 를 읽거나 쓰지 마라. 이로 인해 컴포넌트를 예측하기가 어려워진다.
profile
반갑습니다.

0개의 댓글