Refs로 값 참조하기

파이리·2023년 7월 27일

React

목록 보기
7/9
post-thumbnail

원문 : https://react.dev/learn/referencing-values-with-refs


컴포넌트가 특정 정보를 기억하도록 하고 싶지만, 해당 정보가 랜더링을 트리거하지 않도록 하려는 경우 참조를 사용할 수 있습니다.

컴포넌트에 참조 추가하기

React에서 useRef Hook을 가져와 컴포넌트에서 ref를 추가할 수 있습니다. 컴포넌트 내에서 useRef를 호출하고 참조하려는 초기 값을 인수로 전달합니다.

const ref = useRef(0);

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

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

ref.current 프로퍼티를 통해 해당 참조의 현재 값에 접근할 수 있습니다. 이 값은 변경 가능하므로 읽기 / 쓰기 모두 가능합니다. 이는 React가 추적하지 않는 비밀 주머니같은 것으로 React의 단방향 데어터 흐름에서 탈출구 역활을 합니다.

refstate와 마찬가지로 문자열, 객체, 함수 등 무엇이든 가리킬 수 있습니다. state와 달리 ref는 현재 속성을 읽고 수정할 수 있는 JavaScript 객채로 랜더링을 트리거하지 않습니다.

예제 : 스톱워치 만들기

refstate를 단일 컴포넌트로 결합 할 수 있습니다. 예를 들어 사용자가 버튼을 눌러 시작 / 중지하는 스톱워치를 만들어봅시다. 사용자가 시작을 누른 후 얼마나 시간이 지났는지 표기하려면 시작 버튼을 누른 시점과 현재 시간을 추적해야 합니다. 또한 시간이 얼마나 지났는지 사용자에게 보여줘야됨으로 랜더링에 사용되는 상태로 관리해야 합니다.

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

사용자가 시작 버튼을 누르면 10 밀리초마다 시간을 업데이트하기 위해 setIntrerval을 사용하게 됩니다.

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

정지 버튼을 누르면 state 업데이트를 중지하도록 setInterval을 취소해야 합니다. 이는 clearInterval을 호출하여 할 수 있지만, 사용자가 시작을 눌렀을 때 setInterval에서 반환한 ID를 인수로 전달해야 합니다. 따라서 setInterval ID를 어딘가에 저장해야 합니다. ID는 랜더링에 사용되지 않으므로 ref에 보관할 수 있습니다.

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

랜더링에 사용되는 정보는 state로 관리하세요. 이벤트 핸들러에서 정보를 필요로하고 변경해도 랜더링에 할 필요가 없는 경우는 ref로 관리하는 것이 효율적일 수 있습니다.

ref와 state의 차이

refsstate
useRef는 current 프로퍼티를 가진 객체를 반환합니다.useState는 현재 상태와 상태를 변경할 수 있는 set 함수를 배열로 반환합니다.
값을 변경해도 리랜더링을 트리거 하지 않습니다.값이 변경되면 리랜더링이 트리거됩니다.
변경 가능합니다. ( 랜더링 프로세스 외부에서 current 값을 수정하고 업데이트할 수 있습니다. )변경 불가합니다. ( 상태를 바꾸기 위해서는 set 함수를 통해 변경하고 다시 랜더링 대기열에 추가해야 합니다. )
랜더링 중에서는 현재 값을 읽거나 쓰지 않아야 합니다.언제든지 상태를 읽을 수 있습니다. 각 랜더링에는 변경되지 않은 상태의 스냅삿이 있습니다.

다음 상태로 구현된 카운터 버튼입니다.

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

카운트의 값이 화면에 보여지기 떄문에 상태로 관리하는 것이 합리적입니다. 카운터 값이 변경되면 React는 컴포넌트를 재랜더링하고 화면에 새로운 카운트가 반영되도록 합니다.

만약 이것을 ref를 구현하면 React는 컴포넌트를 다시 랜더링하지 않으므로 카운트가 변경되는 것을 볼 수 없을 것입니다.

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을 읽어오면 코드가 불안정해지는 이유입니다. 이 경우 state를 대신 사용하세요

ref를 사용하는 경우

일반적으로 컴포넌트가 React의 외부에서 컴포넌트의 모양에 영향을 주지 않는 브라우저 API와 통신해야할 때 ref를 사용합니다. 다음은 이런 드문 상황에 대한 예시입니다.

  • 타이머 ID 저장
  • 다음 페이지에서 다룰 DOM 엘리먼트 저장 및 조작하기
  • JSX 계산에 필요하지 않은 다른 객체를 저장합니다.

컴포넌트에 일부 값을 저장해야 하지만 랜더링 로직에 영향을 주지 않을 경우 참조를 선택합니다.

ref를 위한 모범 사례

이러한 원칙에 따라 ref를 사용하면 컴포넌트를 더욱 예측 가능하게 만들 수 있습니다.

  • 참조를 탈출구 취급하세요. 참조는 외부 시스템이나 브라우저 API로 작업할 때 유용합니다. 애플리케이션 로직과 데이터 흐름의 대부분이 참조에 의존한다면 접근 방식을 재고해보세요.
  • 랜더링 중에서는 ref.curret를 읽거나 쓰지 마세요. 랜더링 중 일부 정보가 필요하다면 state를 대신 사용하세요. React는 ref.curret가 언제 변경되는지 알 수 없기 때문에 랜더링 중에 읽거나 쓰면 컴포넌트의 동작이 에측하기 어려워 집니다.

React state의 제약사항은 참조에는 적용되지 않습니다. 예를 들어 state는 모든 랜더링에 대해 스냅샷 처럼 작동하며 동기적으로 업데이트되지 않지만, 참조는 현재 값을 변경하면 즉시 변경됩니다.

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

이는 ref 자체가 일반 자바스크립트 객체이므로 일반 자바스크립트 객체처럼 동작하기 떄문입니다.

또한 참조로 작업할 떄 변형을 피하는 것에 대해 걱정할 필요가 없습니다. 변경하려는 객체가 랜더링에 사용되지 않는 한, React는 참조나 참조의 내용을 무엇을 하든 상관하지 않습니다.

Ref와 DOM

참조는 모든 값을 가리킬 수 있습니다. 그러나 참조의 가장 일반적인 사용 사례는 DOM요소에 접근하는 것입니다. 예를 들어 입력에 포커스를 주고자 할 떄 유용합니다.<div ref={myRef}>와 같이 JSX에서 ref 어트리뷰트에 ref를 전달하면 React는 해당 DOM 엘리먼트를 myRef.current에 넣습니다.

요약

  • 참조는 랜더링에 사용되지 않는 값을 유지하기 위한 탈출구입니다. 자주 필요하지 않습니다.
  • refcurrent라는 단일 프로퍼티를 지닌 일반 자바스크립트 객체로, 읽거나 쓸 수 있습니다.
  • useRef Hook를 호출하여 React에 ref를 제공하도록 요청할 수 있습니다.
  • state와 마찬가지로 ref를 사용하면 컴포넌트의 재랜더링 사이에 정보를 유지할 수 있습니다.
  • state와 달리 ref를 현재 값을 새로 설정해도 리랜더링이 트리거되지 않습니다.
  • 랜더중에는 ref.current를 읽거나 쓰지마세요. 이는 컴포넌트의 동작을 예측하기 어렵게 합니다.

useRef는 내부에서 어떻게 동작하나요?

useStateuseRef 모두 React에서 제공되지만, 원칙적으로는 useRefuseState 위에 구현될 수 있습니다. React 내부에서는 useRef를 다음과 같이 구현한다고 상상할 수 있습니다.


**function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}**

초기 랜더링 중에 useRef{ current: initialValue } 를 반환합니다. 이 객체는 React에 의해 저장되므로 다음 랜더링 중에도 동일한 객체가 반환됩니다. 이 예제에서는 statesetter가 어떻게 동작하는지 주목하세요. useRef는 항상 동일한 객체를 반환해야 하기에 setter는 불필요합니다.

Ref를 사용하는 것을 충분히 일반적이기에, React에서는 빌트인 버전의 useRef를 제공합니다. 하지만, 이는 setter가 없는 일반 상태 변수라고 생각하면 됩니다.

profile
프론트엔드 개발자

0개의 댓글