React Ref 언제 사용해야할까?

그릿 Grit·2025년 3월 13일
0

react

목록 보기
1/1
post-thumbnail

💫 Ref란

React에서 ref는 어떤 형태의 값이든 저장할 수 있는 변수로 생각할 수 있다.
React에서 useRef Hook을 가져와 컴포넌트에 ref를 추가할 수 있다.

import { useRef } from 'react';
function Component (){
  const ref = useRef(0); // 초기값을 인자로 전달
  
  return <></>
}

아래는 ref의 구조이다.

{
  current: 0  // <- 저장하고 싶은 값을 useRef를 통해 전달한다
}

ref는 읽고 수정할 수 있는 current 프로퍼티를 가진 일반 자바스크립트 객체이다.
ref는 state와 달리 값이 변해도 렌더링을 트리거하지 않으며, 값을 변경하면, 바로 변경이 반영된다.

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

🤔 언제 사용해야할까?

1) 값을 저장 싶을 때 (리렌더링 없이)

값을 저장할 변수가 필요한데, 컴포넌트가 이를 기억할 필요가 있을 때

컴포넌트 내부에 일반 변수를 선언해서 사용하면, 컴포넌트가 리렌더링된 후에, 그 값을 기억하지 못합니다. 따라서 이때 ref를 사용하는 것이 적절하다.
다만, 주의해야할 것은 ref의 값이 변할 때, 렌더링을 발생시키지 않는다는 점이다.
(반면, state는 값이 변할 때마다, 리렌더링한다.)

위 예시에서, 컴포넌트는 countRef.current가 증가할 때마다 다시 렌더링 되지 않는다.
위에서 ref 값을 증가시키는 버튼을 누르면, ref.current 값은 변한다. 하지만, 리렌더링이 되지 않기 때문에, 화면에 출력되는 ref.current에는 변화가 없다.
실제 ref.current가 증가되었는지 궁금하다면, alert로 확인하기 버튼을 눌러서 확인할 수 있다. 그러면 ref.current가 버튼을 누른 횟수만큼 증가했다는 사실을 알 수 있다.

예시) 타이머 조작

import { useState } from "react";

export default function TimerChallenge({ title, targetTime }) {
  const [timerExpired, setTimerExpired] = useState(false);
  const [timerStarted, setTimerStarted] = useState(false);

  // 게임 시작 (타이머 작동 시작 및 게임 시작)
  const handleStart = () => {
    // 상태 값으로 끝난 것만 알 수 있다.
    setTimeout(() => {
      // target time 후에 타이머가 끝난걸 표시
      setTimerExpired(true);
    }, targetTime * 1000);

    // 시작했다는 것을 표시하기 위한 상태 값
    setTimerStarted(true);
  };

  // 사용자가 타이머를 멈췄을 때
  const handleStop = () => {
    // 타이머를 끝내야 한다. (타이머를 어떻게 접근? 🤔)
  };

  return (
    <section className="challenge">
      <h2>{title}</h2>
      <p className="challenge-time">{targetTime}</p>
      {timerExpired && <p className="challenge-time">타이머가 끝났어요 😢</p>}
      <p>
        <button onClick={handleStart}>
          {timerStarted ? "멈추기" : "시작하기"}
        </button>
      </p>
      <p className={timerStarted ? "active" : undefined}>
        {timerStarted ? "타이머 진행 중..." : "타이머 정지"}
      </p>
    </section>
  );
}

지정된 시간 만큼 지났을 때를 유추해서, 사용자가 정지 버튼을 누르는 게임이다. 사용자가 "정지"버튼을 눌렀을 때 시작된 타이머에 접근할 수 있어야 하는데, 어떻게 해야할까?
이때 ref를 사용하기 적절하다.

export default function TimerChallenge({ title, targetTime }) {
  const [timerExpired, setTimerExpired] = useState(false);
  const [timerStarted, setTimerStarted] = useState(false);

  const timer = useRef();

  // 게임 시작 (타이머 작동 시작 및 게임 시작)
  const handleStart = () => {
    // 상태 값으로 끝난 것만 알 수 있다.
    timer.current = setTimeout(() => {
      // target time 후에 타이머가 끝난걸 표시
      setTimerExpired(true);
    }, targetTime * 1000);

    // 시작했다는 것을 표시하기 위한 상태 값
    setTimerStarted(true);
  };

  // 사용자가 타이머를 멈췄을 때
  const handleStop = () => {
    // javascript에서 clearTimeout을 통해 타이머를 끝낼 수 있다. 하지만 인자로 ID가 필요하다
    // → 해당 값을 ref에 저장
    clearTimeout(timer.current);
  };
  
  // ... 생략 ...
  

관리되어야하는 값이 있는데, timerID와 같이 ui와 직접적인 연관이 없어서 렌더링이 필요하지 않은 경우 ref를 쓰는 것이 좋다.

2) DOM 요소에 접근하고 싶을 때

DOM 요소에 접근하여, DOM 요소 속성의 값(input의 value, 요소의 크기 등)을 읽어오거나, 포커싱, 스크롤링 등 브라우저 API 함수를 가져와 실행시키고 싶을 때

ref는 문자열, 객체, 심지어 함수 등 모든 것을 가리킬 수 있다. 만약에 ref의 값을 html 요소와 같은 DOM 노드를 가리키도록 넣어주면, DOM 조작에 활용할 수 있다.

input 입력에 대한 조작을 할 때, 일반적으로 useState를 활용해서 구현할 수 있다. 하지만, 이렇게 조작하면, 사용자가 input에 값을 입력할 때마다 리렌더링된다.
만약, 특정 이벤트(폼 제출, 버튼 클릭 등)**에만 값을 input에서 읽어오는 것으로 충분하다면 어떻게 하면 될까?

import { useRef, useState } from "react";

export default function Player() {
  // 초기화를 안하면, 첫 렌더링 시에는 playerRef.current는 undifined 
  const playerRef = useRef(); // 보통, DOM 요소를 저장할거면, null로 초기화
  const [playerName, setPlayerName] = useState();

  const handleClick = (event) => {
    // 이벤트 핸들러에서 ref에서 값을 가져올 수 있다.
    setPlayerName(playerRef.current.value);
  };

  return (
    <section id="player">
      <h2>안녕하세요, {playerName ?? "OOO"}!</h2>
      <p>
        <input type="text" ref={playerRef} />
        <button onClick={handleClick}>이름 저장</button>
      </p>
    </section>
  );
}

위 예제에서는 "이름 저장 버튼"을 눌러줄 때, input의 값을 읽어서 반영해줄 수 있다. 즉, 버튼을 클릭했을 때, input DOM 노드에 직접 접근하여 value 값을 가져온다. state와 함께 사용한 이유는, 단순히 화면에 보여주기 위함이다.

일반적으로, React가 렌더링 결과물에 맞춰 DOM을 직접 조작하도록 설계되어 있기 때문에, DOM을 직접 조작하는 일은 많이 없다.
→ 즉, 일반적인 case에서 DOM 조작은 react의 일!

따라서 DOM 노드를 수정하거나 제거하기보단, 아래 상황일 때 ref를 사용하게 될 것이다.

1) DOM 노드에 접근해 값을 읽고 싶을 때(주로 이벤트 핸들러에서)
2) 노드에 정의된 내장 브라우저 API를 사용하고자 할 때

// 이벤트 핸들러에서 DOM 노드를 접근하기 위해 사용
function handleClick() {
  inputRef.current.focus();
}
// 예를 들어 이렇게 브라우저 API를 사용하거나,
myRef.current.scrollIntoView();

Ref로 DOM을 직접 조작해도 될까?

import { useRef, useState } from "react";

export default function Player() {
  const playerRef = useRef();
  const [playerName, setPlayerName] = useState();

  const handleClick = (event) => {
    // 이벤트 핸들러에서 ref에서 값을 가져올 수 있다.
    setPlayerName(playerRef.current.value);
    playerRef.current.value = "";
  };

  return (
    <section id="player">
      <h2>안녕하세요, {playerName ?? "OOO"}!</h2>
      <p>
        <input type="text" ref={playerRef} />
        <button onClick={handleClick}>이름 설정</button>
      </p>
    </section>
  );
}

위 코드를 보면, 브라우저에게 input의 값을 빈 문자열로 바꾸라고 명령하고 있다. 직접 조작하지 않고,React가 DOM과의 상호 작용과 관련된 역할을 일임한다는, 개념을 위반하고 있다.

그럼에도 불구하고 그저 input을 지우고 싶은 경우에는, input이 state와 연결되어 있지 않으니, 위처럼 그냥 코드를 작성해봐도 된다. (코드 양을 줄일 수 있다.)
다만, 페이지의 모든 값들을 ref로 읽고 조작하는데 사용하지 않도록 유의하여야 한다.
특히 노드를 삭제하는 행위는, 충돌을 일으킬 수 있으므로 유의해야한다. 공식문서에 있는 codesandbox를 통해 직접 확인해볼 수 있다.

결론 (Ref 사용법)

✅ 렌더링을 유발하지 않고 값을 저장해야 할 때
예: 타이머 ID, 이전 렌더에서 유지해야 하는 값 등

✅ DOM 요소를 직접 제어해야 할 때
예: input 값 가져오기, 포커스 주기, 스크롤 제어 등

⚠️ 주의사항
❌ 값의 변경 시 리렌더링이 필요하면 사용하면 안 됨.
❌ DOM 노드 삭제 또는 직접 조작(값 변경 등)은 주의해야 함.

profile
𝙒𝙝𝙚𝙧𝙚 𝙩𝙝𝙚𝙧𝙚 𝙞𝙨 𝙖 𝙬𝙞𝙡𝙡 𝙩𝙝𝙚𝙧𝙚 𝙞𝙨 𝙖 𝙬𝙖𝙮 ✨

0개의 댓글