Ref 뽀개기 - NEW 리액트 공식문서

hongregii·2023년 3월 20일
0

컴포넌트가 어떤 정보를 "기억" 하게 하고 싶지만, 리렌더링은 안했으면 좋겠을 때 - 그 때 ref 를 쓰자.

이 문서에서는..

  • 컴포넌트에 ref 더하기
  • ref 값 업데이트하기
  • ref vs state
  • ref 안전하게 쓰는 법
    을 배워보자.

컴포넌트에 ref 더하기

리액트에서 useRef 훅을 임포트하자.

import { useRef } from 'react';

컴포넌트 안에서 useRef 훅을 호출하고, 인자로 ref의 초기값을 넘겨주자. useState의 인자랑 개념이 비슷. 그러나 setState 함수가 리턴으로 튀어나오지는 않음.

const ref = useRef(0);

useRef는 객체 하나를 리턴한다 :

{
  current : 0 // useRef의 인자로 넘겨준 초기값
}

현재 ref의 값은 ref.current property에 있다. 이 값은 mutable. 읽고 쓸 수 있다는 뜻이다. 리액트가 따라다니지 않는 "컴포넌트의 비밀 주머니" 같은 녀석이다. (그래서 리액트의 일방향 데이터 플로우로부터의 비상구 escape hatch 역할을 한다는 것임 - 아래에서 계속)


이 버튼은 클릭 마다 `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>
  );
}

( 버튼을 누르면 누른 횟수를 기억하여 그만큼 alert 창이 뜸. 리렌더링은 일어나지 않는다)

ref는 state처럼 아무 값을 가리켜도 된다 : 문자열, 객체, 함수까지도! state와 달리, ref는 일반 JS 객체다 - 읽고 수정할 수 있는 current property가 있는.

버튼이 눌려도 컴포넌트가 리렌더링되지 않는다는 사실을 주목하라. state처럼, ref는 리렌더링 사이에도 React에 의해 유지된다. 그러나 setState는 리렌더링을 유발하지만 ref는 그렇지 않는다는 것.

예시 : 스톱워치 만들기

한 컴포넌트에서 ref와 state를 같이 쓸 수도 있다. 예를 들어, 사용자가 버튼을 누르면 시작하거나 멈추는 스톱워치를 만들어보자. "Start"가 눌리고 난 뒤 얼마나 지났는지를 보여주기 위해, Start 버튼이 언제 눌렸는지와 현재 시간이 언제인지를 계속 알고 있어야 한다. 이 정보들은 렌더링에 사용되니까, State에 넣어두자:

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

사용자가 "Start"를 누르면, setInterval로 10 밀리초마다 시간을 업데이트하자.

// App.js

import { useState } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null); // 시작 누른 시간 state
  const [now, setNow] = useState(null); // 현재 시간 state

  function handleStart() {
    // 카운트 시작
    setStartTime(Date.now());
    setNow(Date.now());

    setInterval(() => {
      // 10ms마다 현재 시간 state만 업데이트 &rarr; 리렌더링 &rarr; 컴포넌트 함수재실행
      setNow(Date.now());
    }, 10);
  }

  let secondsPassed = 0; // let이라서 setNow() 마다 컴포넌트 리렌더링, 함수 재실행 => 매 setState마다 let은 초기화
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  } // 근데 이것도 재실행

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
    </>
  );
}

Start 버튼은 구현됐다. 이제 Stop 버튼을 만들어보자. Stop이 눌리면, 현재 Interval을 취소해서 now state를 그만 업데이트 해야 한다. clearInterval을 사용하면 되지만, Start를 눌렀을 때 호출된 setInterval에서 리턴된 interval ID를 넘겨줘야 한다. interval ID를 어딘가에서는 저장해둬야 한다는 뜻이다. 이 interval ID는 렌더링에 사용되지 않기 때문에, ref에 둘 수 있다 :

// App.js

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null); // 여기서 null로 선언

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

    clearInterval(intervalRef.current); // stop 후 다시 눌렸을 때를 대비
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10); // 여기서 interval ID 리턴되는 듯 (setInterval(()=>{}, 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 vs state

ref가 state보다 덜 "엄격"하다고 생각할 수 있다 - 예를 들어 setState를 사용하지 않고 직접 바꿔도 되는 것. 그러나 대부분의 경우에 state를 사용해야 할 것이다. Ref는 자주 사용하지 않을 비상구이다. 이 표를 보시라:

refsstate
useRef(initialValue) returns { current: initialValue }useState(initialValue) returns 현재 state와 state setter 함수 ( [value, setValue])
바꿔도 리렌더링 안됨바꾸면 리렌더링 됨.
Mutable — 렌더링 프로세스 밖에서 값 수정 가능current“Immutable”—리렌더링을 하기 위해서 setState 함수를 사용해야만 값을 바꿀 수 있음.
렌더링 중에는 current 값을 읽거나 쓰거나 수정하면 안된다!아무때나 읽을 수 있다 - 대신에 각 렌더링은 변하지 않는 state의 스냅샷을 각자 가지고 있다.

렌더링 중에는 current 값을 읽거나 쓰거나 수정하면 안된다는 게 무슨 말이냐?

누르면 누른 횟수가 늘어나는 버튼을 만들 때, ref를 써서 숫자를 늘려도 렌더링에는 늘어난 숫자가 보이지 않을 것이다 (= 몇번을 눌러도 0일 것). 리렌더링이 되지 않았기 때문임. 화면에 뿌려줄 값을 바꿔야 할 때는 항상 state를 써라.

useRef 내부 코드 뜯어보기

사실 useRefuseState 를 감싸고 있다. 실제로 이렇게 만들어져 있지는 않지만, 이렇게 돼 있다고 생각하면 됨 :

// 리액트 내부코드 ! (라고 생각하세요)
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}

첫 렌더에서 useRef{ current : initialValue }를 리턴한다. 리액트가 이 객체를 저장하기 때문에 다음 렌더링에는 같은 객체가 리턴될 것이다. setState 함수가 unused로 선언된 것을 주목해라. 항상 같은 객체를 리턴해야만 하기 때문에 setter 함수는 필요가 없다는 것.

언제 씀?

일반적으로 컴포넌트가 리액트 "밖"으로 나가고 외부 API ( 주로 컴포넌트의 모습에 영향을 주지 않는 브라우저 API )를 사용할 때 ref를 쓰게 될 것이다. 예를 들어 :

  • timeout ID 저장
  • DOM 요소 저장 및 조작 - 다음 장에서 다룰 것임
  • JSX 계산에 필요하지 않은 객체 저장

컴포넌트가 어떤 정보를 저장해야 하는데 렌더링 로직은 건드리고 싶지 않으면 ref를 선택해라.

ref, 이때 최고

아래 규칙을 따라가면 컴포넌트의 작동이 더 깔끔해질 것이다 :

  • ref는 비상구 : ref는 리액트 외부 시스템이나 브라우저 API를 다룰 때 좋다. 앱의 핵심 로직에 ref가 너무 많이 들어가 있으면 다시 생각해 보는 것이 좋을 것이다.

  • 렌더링 중 ref.current를 읽거나 쓰지 말 것. 위에서 적었듯, 화면에 필요한 정보는 state를 써라. 리액트는 ref.current가 바뀌는지 안바뀌는지 모른다. 심지어 렌더링 중에 읽어오면 의도대로 작동을 안 할 것임.

    물론 첫 렌더때만 ref를 설정해주는 이런 코드는 잘 작동함. 이렇게.

if (!ref.current) {
  ref.current = new Thing()
}

ref에서 state의 단점이 적용되지는 않는다. 예를 들어, state는 각 렌더의 스냅샷처럼 작동하고, 비동기로 업데이트된다. 그러나, ref를 바꾸면 바로 바뀐다.

ref.current = 5;
console.log(ref.current); // 바로 5 출력

ref가 일반 JS 객체기 때문에 그렇게 행동하는 것.

mutation 안쓰기에 대해 걱정할 필요도 없다. spread 연산자 안써도 됨. 렌더링과 관련 없기 때문에 리액트는 ref의 변화에 관심이 하나도 없다.

Ref 와 DOM

ref는 아무 값을 가리켜도 좋다. 그러나, 보통은 DOM 요소를 가리킨다. 예를 들어, 인풋에 focus를 프로그램적으로 넣고 싶을 때 좋음.
<div ref={myRef}> 처럼 JSX의 ref 속성에 ref를 넣어주면, 리액트는 해당 DOM 요소를 myref.current 에 넣어줄 것이다. 자세한 설명은 뒤에서 계속

6줄 요약

  • ref는 비상구다. 렌더링이랑 관계 없이 동작함. 자주 볼 일이 없을 것이다!
  • ref는 일반 JS 객체다. property는 단 하나 : current. 읽고 쓸 수 있다.
  • useRef 훅을 써서 호출.
  • state처럼 리렌더링해도 변수를 살려둘 수 있음.
  • state와 달리, current를 수정해도 리렌더링이 안생김.
  • 렌더링 중에 ref.current를 읽거나 쓰지 마라. 컴포넌트가 예측 불허가 된다..
profile
잡식성 누렁이 개발자

0개의 댓글