본문 바로가기

나야, 리액트 스터디

[week6] Ref로 값 참조하기

안녕하세요 웹 YB 김다현입니다.

어제 다들 잘 들어가셨나요?,,, 🐒 벌써 6주차라니 시간이 참 빠루네용 이번 주차엔 ref에 대해 공부해봤는데요! 

항상 ref에 대한 개념이 부족하다고 생각했어서 이번 기회에 아티클을 통해 더 꼼꼼히 공부해보려고 합니다! 💥 

 

 


 

Ref로 값 참조하기

리액트 컴포넌트를 만들다 보면 '어떤 값이나 상태를 기억하고 싶지만, 이걸 바꾼다고 해서 화면이 다시 그려지는 건 싫어!' 라는 순간이 올 텐데요. 이럴 때 ref를 사용하면 됩니다.

 

그럼 Ref가 뭔데?

ref는 리액트가 제공하는 특별한 도구로 컴포넌트 안에서 값을 저장해두는 비밀 주머니 ㅋㅋ 라고 생각하면 돼요. 이 주머니는 값이 바뀌어도 화면을 다시 그리지 않습니다.

 

1️⃣ 컴포넌트에 Ref 추가하기

리액트에서는 useRef라는 Hook을 사용해서 ref를 만들 수 있어요. 그러려면 우선 useRef를 가져와야겠죠?

import { useRef } from 'react';

 

그 다음 컴포넌트 안에서 useRef를 호출해요. 초기값 (ex. 숫자 0)을 넣어주면 이 값을 기억하는 ref가 만들어집니다.

const ref = useRef(0);

 

만들어진 ref는 이런 형태로 생겼어요.

{
  current: 0
}

 

여기서 current라는 프로퍼티에 아까 설정한 초기값이 들어가요. 이 current가 바로 우리가 읽고 쓸 수 있는 비밀 공간이랍니다...🧙‍♀️

 

잠깐! ref.current란?

 

리액트에서 useRef로 만든 ref는 특별한 객체를 반환합니다.

이 객체 안에는 current라는 프로퍼티가 하나 있는데 이 current가 바로 값을 저장하고 관리할 수 있는 공간이에요!

 

어떻게 동작하지?

ref.current를 통해 값을 읽을 수도 있고 원하는 대로 바꿀 수도 있어요.

말 그대로 '내가 마음대로 조작할 수 있는 비밀 공간' 이라고 생각하면 됩니다!

React는 이 값을 추적하지 않아요! 즉 내가 값을 변경해도 화면이 다시 렌더링 되지 않아요.

 

React의 단방향 데이터 흐름에서 탈출구

리액트는 일반적으로 state를 통해 데이터를 관리하는데 state를 변경하면 항상 컴포넌트가 다시 렌더링돼요.
하지만 ref는 값을 바꾸더라도 컴포넌트가 다시 렌더링 되지 않아요.

즉, React의 단방향 데이터 흐름을 잠시 벗어나는 탈출구로 활용할 수 있답니다!

 

2️⃣ State와 Ref, 둘 다 사용할 수 있을까?

ref와 state는 서로 다르게 동작하지만 때로는 둘 다 사용해서 적절히 상황을 처리할 수 있습니다. 스톱워치 예제를 통해 살펴볼까요!

 

기본 동작

사용자가 시작 버튼을 누르면 시간이 흐르기 시작하고, 흐른 시간을 화면에 표시하는 스톱워치를 만들려면 현재 시각시작 시각을 기억해야겠죠?

이 정보들은 화면에 보여줘야 하니까 state로 관리하면 돼요.

 
import { 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(() => {
      // 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>
    </>
  );
}

 

 

동작 원리

- 사용자가 시작 버튼을 누르면 startTime에 시작 시각을 저장하고 now를 현재 시각으로 업데이트해요.

- setInterval로 10ms마다 now를 업데이트하면서 흐른 시간을 계산해요.

- 화면은 state의 변화로 자동으로 갱신됩니다!

 

3️⃣ Stop 버튼을 추가해볼까?

stop 버튼을 추가하려면 시계가 흐르지 않도록 setInterval을 멈춰야겠죠?! 여기서 등장하는 게 ref입니다.

 

왜 ref를 사용하는 걸까?

setInterval이 반환하는 interval 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); // interval ID를 저장할 ref

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

    // 기존 interval 정리 (중복 방지)
    clearInterval(intervalRef.current);

    // interval ID 저장
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    // interval 정리
    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>
    </>
  );
}

 

▶️ Start 버튼

- setInterval로 시간이 흐르기 시작

- interval ID를 intervalRef.current에 저장

 

✋🏻 Stop 버튼

- clearInterval(intervalRef.current)를 호출해 interval을 정리

- 시간이 멈추고 화면 갱신도 멈춤

 

그래서 언제 state, 언제 ref를 사용하는 거라고?

state -> 화면에 보여줄 데이터는 항상 state에 저장!

ref -> 렌더링에 사용되지 않고 이벤트 처리나 외부 데이터를 관리할 땐 ref를 사용!

 

방금 스탑워치 예제에서,

 

startTimenow는 화면에 표시되는 데이터 → state

interval ID는 렌더링과 무관한 데이터 → ref

 

ref와 state의 차이?

refs state
useRef(initialValue)  { current: initialValue } 을 반환합니다. useState(initialValue) 은 state 변수의 현재 값과 setter 함수 [value, setValue] 를 반환합니다.
state를 바꿔도 리렌더 되지 않습니다. state를 바꾸면 리렌더 됩니다.
Mutable-렌더링 프로세스 외부에서 current 값을 수정 및 업데이트할 수 있습니다. ”Immutable”—state 를 수정하기 위해서는 state 설정 함수를 반드시 사용하여 리렌더 대기열에 넣어야 합니다.
렌더링 중에는 current 값을 읽거나 쓰면 안 됩니다 언제든지 state를 읽을 수 있습니다. 그러나 각 렌더마다 변경되지 않는 자체적인 state의 snapshot이 있습니다.

 

 

ref는 덜 엄격하다는 게 무슨 뜻이지?

- state는 React가 엄격하게 관리하는 데이터예요. 값을 바꾸려면 꼭 setState 같은 함수를 써야 하고 값을 바꾸면 화면이 다시 그려집니다.

- 반면에 ref는 덜 엄격하기 때문에 그냥 ref.current에 값을 넣으면 끝이에요. React는 '어? 이 값이 바뀌었네?' 하고 넘어갈 뿐, 다시 화면을 그리진 않습니다.

 

State와 Ref의 차이를 하나씩 살펴보자

리렌더링 여부

state -> 값을 바꾸면 화면이 다시 그려져요

ref -> 값을 바꿔도 화면은 그대로! 리렌더링 되지 않아요

 

값 수정 방법

state -> 값을 바꾸려면 반드시 setState 함수를 써야 해요. React가 '내가 화면을 다시 그려줄게!' 하고 관리해줍니다.

ref -> 그냥 ref.current에 값을 넣으면 돼요. React가 관여하지 않아요.

 

렌더링 중 사용

state -> 렌더링 중에도 안전하게 읽을 수 있습니다. 렌더마다 독립적인 '스냅샷'을 갖고 있기 때문!

ref -> 렌더링 중에는 값을 읽거나 쓰지 않는 게 좋아요. 잘못된 값이 나올 수 있어요.

 

✅ State로 구현한 버튼

import { useState } from 'react';

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

  function handleClick() {
    setCount(count + 1); // state 값을 변경
  }

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

 

 

 

🤔 왜 state를 써야 할까?

- 버튼 클릭 횟수(count)는 화면에 표시되니까 React가 값을 추적하고 리렌더링해야 합니다.

- setCount로 값을 바꾸면 React가 컴포넌트를 다시 렌더링해서 최신 값을 화면에 보여줘요.

 

❌ Ref로 구현한 버튼

import { useRef } from 'react';

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

  function handleClick() {
    countRef.current = countRef.current + 1; // ref 값 변경
  }

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

 

 

🤔 이 코드의 문제는? 

- 버튼을 클릭해도 화면이 업데이트 되지 않아요.

- 왜냐하면 ref를 바꿔도 컴포넌트가 다시 렌더링되지 않기 때문이에요. 결국 countRef.current 값을 바뀌지만 화면에는 반영되지 않겠죠?

 

한 번 정리해보자면!

1️⃣ state는 화면에 영향을 주는 데이터를 관리합니다. 값을 바꾸면 컴포넌트를 다시 렌더링해요.

2️⃣ ref는 화면에 영향을 주지 않는 데이터를 관리합니다. 값을 바꿔도 화면은 그대로예요.

3️⃣ 렌더링 중에는 ref 값을 다루지 말고, 화면에 표시되는 데이터라면 state를 사용하기!

 

useRef는 내부적으로 어떻게 동작하나요?

 

이건 공식문서에 딥다이브 내용으로 정리되어있던 부분이었는데 진심 뭐라고 하는 건지 이해가 1도 안 가서 ;; 쉽게 풀어서 이해해보려고 애썼습니다......

 

useRef가 내부적으로 어떤 구조를 가지고 있을까?

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

 

React는 사실 useRef를 아주 간단하게 만들었어요. 내부적으로는 useState를 약간 변형한 것이라고 생각하면 돼요.

 

✅ useRef는 내부적으로 state처럼 동작해요.

- useState를 써서 { current: initialValue }라는 객체를 생성해요.

- 여기서 { current: initialValue }가 바로 우리가 쓰는 ref의 기본 형태예요.

 

✅ React는 이 객체를 컴포넌트의 렌더링과 함께 유지해줘요.

- 렌더링이 반복되더라도 항상 같은 객체를 반환해요.

 

✅ 하지만 state와 달리 화면을 다시 그리지 않아요.

useState처럼 값을 바꾸기 위한 setter 함수(setValue)를 만들지 않아요.

 

왜 setter가 필요 없을까?

 

useRef는 항상 같은 객체를 반환하니까 값이 바뀌면 그냥 ref.current를 직접 수정하면 돼요!

const myRef = useRef(0);
myRef.current = myRef.current + 1;

 

 

반면에 state는 값이 바뀌면 렌더링을 다시 해야 하기 때문에 setter 함수가 반드시 필요해요.

const [count, setCount] = useState(0);
setCount(count + 1); // 이건 컴포넌트를 리렌더링!

 

useRef는 React 내부에 숨겨둔 작은 메모장 같은 거예요. React가 메모장을 만들어 놓고, 필요할 때마다 꺼내 쓸 수 있게 해주는 느낌!

첫 번째 렌더링 때 메모장에 초기값을 적어두고 이후에는 메모장을 바꿔치기하지 않고 계속 같은 메모장을 넘겨줍니다.

 

 

요악하자면?

1️⃣ useRef는 React가 내부적으로 { current: initialValue }라는 객체를 생성해 반환하는 구조예요.

2️⃣ state와 다르게 setter 함수가 필요 없고, 직접 값을 바꾸면 돼요.

3️⃣ React가 항상 같은 객체를 반환하므로 렌더링과 상관없이 데이터를 유지할 수 있어요.

4️⃣ state는 화면이 바뀌는 데이터를 관리하고 ref는 렌더링과 무관한 데이터를 관리합니다.

 


 

❓궁금한 점

React가 ref를 렌더링에서 완전히 제외시키는 이유가 뭔지 갑자기 궁금해지네요... 리액트는 ref는 렌더링과 관련 없는 데이터를 다룬다고 계속 강조하는데 사실 렌더링 중에도 ref 값을 읽고 싶은 순간이 생길 수 있지 않을까? 하는 의문도 들고 왜 리액트는 ref를 렌더링 프로세스에서 철저히 분리하려고 할까요??