React

ref 와 state 차이

D cron 2023. 3. 4. 17:54

본 내용은 리액트 공식문서를 번역한 것으로 오역이나 의역이 있을 수 있습니다.

Refs 활용법

컴포넌트에 Ref를 다는 법

import { useRef } from 'react';

컴포넌트 안에서 useRef를 불러와서 초기값을 지정할 수 있습니다.

const ref = useRef(0);

useRef는 아래와 같은 객체를 반환합니다.

{
    current: 0;
}

current 값에 ref.current 프로퍼티를 이용해서 접근할 수 있습니다.

  • 이 값은 의도적으로 mutable 합니다. 즉, 변화가 가능한 값입니다.
  • 이것은 마치 리액트가 트래킹하지 않는 비밀 주머니와 같습니다.

 

ref는 state 처럼 string, object, function 등을 가리킬 수 있습니다.

ref는 state와 달리 순수한 자바스크립트 객체로 읽고 수정할 수 있습니다.

중요한 점은 ref.current가 바뀐다고 컴포넌트가 re-render 하지 않는다는 점입니다.

ref도 state처럼 리랜더링을 해도 값이 유지되는 특성이 있습니다. 그러나 state가 달라지면 컴포넌트가 리렌더링 되고, ref가 달라지면 리렌더링 되지 않는 차이점이 있습니다.

예제: 스탑워치 만들기

당신은 하나의 컴포넌트에서 refstate를 같이 사용할 수 있습니다. 예를 들어 사용자가 버튼을 클릭해서 시작하고 멈추게 할 수 있는 스톱워치를 만들어봅시다. Start 버튼이 클릭된 이후로 얼마나 많은 시간이 흘렀는지 보여주기 위해서, 언제 Start 버튼이 클릭되었는지와 현재 시간이 언제인지 계속 트래킹을 해야 합니다. 이 정보는 렌더링할 때 사용됨으로 state에 저장해야 합니다.

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

만약 유저가 Start를 누른다면 당신은 매 10ms마다 시간을 업데이트하기 위해 setInterval을 사용할 것입니다.

// Stopwatch.jsx
import React, {useState} from 'react';

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

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

    setInterval(()=>{
      setNow(Date.now()); // 10ms마다 현재 시간 동기화
    },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 버튼이 눌리면, 지금 존재하는 interval을 취소하여 now state가 업데이트되는 것을 멈춰야 합니다. 이것은 clearInterval 을 이용하면 됩니다. 그전에 setInterval로부터 반환된 interval ID를 주어야 합니다. 당신은 interval ID를 어딘가에 저장해야 합니다. 그러나 interval 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); 
    // 연속으로 Start 버튼을 두 번 이상 누를 경우 
    // 여기서 바뀐 intervalRef와 handleStop에서 멈춰줄 intervalRef의 값을 일치시켜야 하기 때문.
    // clearInterval 하면 왜 일치되는가? 
    intervalRef.current = setInterval(()=>{
      setNow(Date.now()); // ref에 10ms마다 현재 시간 동기화
    },10)
  } 

  function handleStop(){
    clearInterval(intervalRef.current); // now 값의 업데이트를 멈춤
  }

    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에 저장하세요. event handling이나 리렌더링에 필요하지 않은 변화를 할 때는 ref를 사용하는 게 더 효율적일 것입니다. (불필요한 리렌더링을 하지 않기 때문)

refs와 state의 차이

당신은 refsstate 보다 덜 제한적 이라고 생각할 수 있습니다 state를 세팅하는 함수를 사용하지 않아도 값을 변경할 수 있기 때문이죠. 하지만 대부분의 경우에서 당신은 state를 쓰고 싶을 겁니다. Refsescape hatch로써 당신이 자주 사용하진 않으니까요.

아래는 staterefs의 비교입니다.

ref

  • useRef(initialValue){ current: initialValue }를 반환합니다.
  • 변경하더라도 리렌더링을 일으키지 않습니다.
  • Mutable : 렌더링 프로세스 밖에서 current값을 수정하고 변경할 수 있습니다. 
  • 렌더링 도중에는 current 값을 읽거나 수정하면 안 됩니다(shouldn’t)

state

  • useState(initialValue)는 현재 값과 state setter 함수로 이루어진 값을 반환합니다. [value, setValue]
  • 변경하면 리렌더링을 trigger 시킵니다.
  • Immutable : state 값을 변경하려면 state setting 함수를 사용해야 합니다.
  • 아무 때나 state의 값을 읽을 수 있습니다. 그러나 매 렌더링 될 때마다 변경되지 않는 state의 snapshot을 가집니다.

 

state로 구현된 counter 버튼의 예시를 살펴보겠습니다.

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 값이 보이고 있으므로 state 값을 사용하는 것이 이해가 됩니다(make sense). counter의 값이 setCount에 의해 설정되기 때문에 리엑트는 컴포넌트를 리렌더링 하고, 화면은 새로운 count를 업데이트해서 반영합니다.

만약 ref으로 구현하려고 하면, 리액트는 절대 컴포넌트를 리렌더 하지 않을 것입니다. 그러니까 절대로 count가 변하는 걸 볼 수 없겠죠!

이게 왜 ref.current의 값을 렌더링 하는 동안 읽어오는 것이 신뢰할 수 없는 코드인지 알려주죠. 만약 이런 경우에는 state를 사용하세요

Deep dive(심화 과정)

useStateuseRef는 둘 다 React에 의해 제공되지만 원칙적으로 useRef는 useState 위에 구현될 수 있습니다. 리액트 내부에서는 useRef가 이렇게 구현되어 있다고 상상해 볼 수 있어요.

// 리엑트 내부...
function useRef(initialValue){
    const [ref, unused] = useState({current: initialValue});
    return ref
}

첫 번째 렌더링에서 useRef{current: initialValue}를 반환합니다. 이 객체는 리액트에 의해 저장되므로 다음 렌더링에도 같은 객체가 반환될 것입니다. state setter가 위의 예제에서 사용되지 않았음을 기억하세요. useRef가 항상 같은 객체를 반환하므로 필요가 없습니다!

그럼 언제 refs를 써야 하나요?

일반적으로, 당신의 컴포넌트가 리액트에서 한 발짝 떨어질 필요가 있을 때 사용됩니다. 그리고 외부의 API들과 소통할 경우 사용됩니다 - 때때로 브라우저 API는 컴포넌트의 외관에 영향을 미치지 않으니까요. 다음과 같이 드문 상황에 사용됩니다.

  • timeoout ID를 저장할 때
  • DOM elements를 저장 및 조작할 때 (다음 페이지에서 설명할 내용)
  • JSX를 계산하는데 필수적이지 않은 객체들을 저장할 때

결론: 만약 컴포넌트가 값을 저장해야 하는데 렌더링 로직에 영향을 주지 않는 경우, refs를 사용하세요

Best practices for refs

아래의 원칙들을 따르는 것은 당신의 컴포넌트를 더욱 예측 가능하게 만들어줄 것입니다.

  • refsescape hatch처럼 여기세요. Refs는 당신의 작업이 외부의 시스템이나 브라우저 APIS와 사용될 때 유용합니다. 만약 애플리케이션 로직이나 데이터의 flow의 대부분이 refs를 사용하고 있다면, 이 접근법을 다시 생각해 보는 게 좋습니다.
  • 렌더링 할 때 ref.current를 읽거나 쓰지 마세요. 만약 어떤 정보가 렌더링 될 때 필요하다면 state를 대신 사용하세요. React는 ref.current의 변화를 감지하지 못하기 때문에, 렌더링 도중에 ref.current 값을 읽는 행위는 당신의 컴포넌트의 동작을 예측하기 어렵게 만들 것입니다. (이런 상황 중에 단 한 가지의 예외사항이 있는데 if(!ref.current) ref.current = new Thing() 와 같은 경우는 첫 렌더링에만 ref를 세팅하므로 괜찮습니다)

 

리액트 state의 한계는 ref에 적용되지 않습니다. 예를 들어, state는 매 렌더링 시마다 snapshot과 같이 동작하고, 변경사항이 바로 반영되지 않습니다(동기화되지 않음). 하지만 만약 ref.current를 변경한다면, 이는 즉시 반영될 것입니다.

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

이것은 왜냐하면 ref 자체가 JS 객체이기 때문입니다.

당신은 ref를 사용할 때 mutation을 피하는 것에 대한 걱정을 할 필요가 없습니다. 변경되는 객체가 렌더링에 사용되지 않기 때문에, 리액트는 ref로 당신이 뭘 하든 신경 쓰지 않습니다.

Refs와 DOM

당신은 ref로 아무 값이나 지정할 수 있습니다. 하지만, 가장 보편적인 경우는 DOM element에 접근하는 경우일 것입니다. 예를 들어, input에 focus 하는 것은 handy 한 일입니다. 만약 ref를 JSX attribute ref에 넘겨서 <div ref={myRef}> 처럼 사용한다면, 리액트는 해당 DOM element를 myRef.current에 저장합니다. 이것은 Manipulationg the DOM with Refs 파트에서 더 자세한 사항을 확인할 수 있습니다.

Recap

  • Refs 는 escape hatch로써 렌더링에 사용되지 않는 값을 저장하는 용도입니다. 자주 쓰이진 않아요.
  • ref 는 current라고 불리는 하나의 property를 가진 순수 JS 객체입니다. 읽고 쓰기가 가능해요.
  • useRef 훅을 이용해서 리액트에게 ref를 달라고 할 수 있어요.
  • state 와 마찬가지로, 리렌더가 일어나도 정보가 유지됩니다.
  • state와 달리 ref의 current를 세팅하는 것은 리렌더를 유발하지 않아요.
  • ref.current를 렌더링 하는 동안에 읽고 쓰지 마세요. 이것은 컴포넌트를 예측하기 어렵게 만드니까요.

'React' 카테고리의 다른 글

리액트 컴파일러 (React Compiler) 알아보기  (1) 2024.04.14