React 독학 가이드 10 - useRef로 변수 생성하기

learning by Seven Fingers Studio 16분
ReactuseRefHooksDOM변수저장

useState 말고 값을 저장하는 방법이 하나 더 있어요. 바로 useRef입니다. 근데 useState랑 뭐가 다르냐고요?

useRef vs useState

핵심 차이:

  • useState: 값이 바뀌면 컴포넌트가 다시 렌더링
  • useRef: 값이 바뀌어도 렌더링 안 됨

언제 useRef를 쓸까요?

  1. DOM 요소에 직접 접근할 때
  2. 렌더링 없이 값을 저장하고 싶을 때
  3. 이전 값을 기억하고 싶을 때

useRef 기본 사용법

import { useRef } from 'react';

function MyComponent() {
  const myRef = useRef(초기값);

  console.log(myRef.current);  // 현재 값 접근
  myRef.current = 새로운값;     // 값 변경 (렌더링 안 됨!)
}

useRef.current 속성을 가진 객체를 반환해요.

활용 1: DOM 요소 접근하기

useRef의 가장 흔한 용도예요. input에 자동 포커스를 주는 예시를 볼게요.

자동 포커스

import { useRef, useEffect } from 'react';

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 컴포넌트가 마운트되면 input에 포커스
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} placeholder="자동 포커스됨!" />;
}

실행 결과:

▲ 페이지 로드 시 자동으로 포커스됩니다

useRef로 DOM 요소에 접근하여 focus() 메서드를 호출합니다

ref={inputRef}로 DOM 요소를 연결하면, inputRef.current로 그 요소에 접근할 수 있어요.

스크롤 위치 제어

import { useRef } from 'react';

function ScrollExample() {
  const topRef = useRef(null);
  const bottomRef = useRef(null);

  const scrollToTop = () => {
    topRef.current.scrollIntoView({ behavior: 'smooth' });
  };

  const scrollToBottom = () => {
    bottomRef.current.scrollIntoView({ behavior: 'smooth' });
  };

  return (
    <div>
      <div ref={topRef}>
        <h1>맨 위</h1>
        <button onClick={scrollToBottom}>맨 아래로 이동</button>
      </div>

      {/* 긴 콘텐츠 */}
      <div style={{ height: '1000px', background: '#f0f0f0' }}>
        <p>스크롤 해보세요...</p>
      </div>

      <div ref={bottomRef}>
        <h1>맨 아래</h1>
        <button onClick={scrollToTop}>맨 위로 이동</button>
      </div>
    </div>
  );
}

비디오 제어

import { useRef } from 'react';

function VideoPlayer() {
  const videoRef = useRef(null);

  const handlePlay = () => {
    videoRef.current.play();
  };

  const handlePause = () => {
    videoRef.current.pause();
  };

  const handleRestart = () => {
    videoRef.current.currentTime = 0;
    videoRef.current.play();
  };

  return (
    <div>
      <video
        ref={videoRef}
        src="video.mp4"
        style={{ width: '100%' }}
      />
      <div>
        <button onClick={handlePlay}>재생</button>
        <button onClick={handlePause}>일시정지</button>
        <button onClick={handleRestart}>처음부터</button>
      </div>
    </div>
  );
}

활용 2: 렌더링 없이 값 저장하기

클릭 횟수 저장 (렌더링 없이)

import { useRef } from 'react';

function ClickCounter() {
  const clickCount = useRef(0);  const handleClick = () => {
    clickCount.current += 1;
    console.log(`클릭 횟수: ${clickCount.current}`);
    // 화면은 업데이트 안 됨!
  };

  return (
    <div>
      <button onClick={handleClick}>클릭하세요</button>
      <p>콘솔을 확인하세요 (화면 숫자는 안 바뀜)</p>
    </div>
  );
}

화면에 보여줄 필요 없고 내부적으로만 추적하고 싶을 때 유용해요.

이전 값 기억하기

import { useState, useRef, useEffect } from 'react';

function PreviousValue() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(0);

  useEffect(() => {
    // 렌더링 후에 현재 값을 저장
    prevCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>현재: {count}</p>
      <p>이전: {prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

타이머/인터벌 ID 저장

import { useState, useRef } from 'react';

function Stopwatch() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef(null);

  const start = () => {
    if (isRunning) return;
    setIsRunning(true);
    intervalRef.current = setInterval(() => {
      setTime((prev) => prev + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
    setIsRunning(false);
  };

  const reset = () => {
    clearInterval(intervalRef.current);
    setTime(0);
    setIsRunning(false);
  };

  const formatTime = (seconds) => {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  };

  return (
    <div style={{ textAlign: 'center' }}>
      <h1 style={{ fontSize: '48px' }}>{formatTime(time)}</h1>
      <button onClick={start} disabled={isRunning}>시작</button>
      <button onClick={stop} disabled={!isRunning}>정지</button>
      <button onClick={reset}>리셋</button>
    </div>
  );
}

실행 결과:

01:23

useRef로 타이머 ID를 저장하여 시작/정지/리셋 기능을 구현합니다

intervalRef로 인터벌 ID를 저장해서 나중에 clearInterval로 정지할 수 있어요.

실전 예제: 입력 포커스 관리

import { useRef, useState } from 'react';

function LoginForm() {
  const [form, setForm] = useState({ username: '', password: '' });
  const [errors, setErrors] = useState({});

  const usernameRef = useRef(null);
  const passwordRef = useRef(null);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm({ ...form, [name]: value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const newErrors = {};

    if (!form.username) {

      newErrors.username = '아이디를 입력하세요';
      usernameRef.current.focus();  // 에러난 필드로 포커스!
    } else if (!form.password) {
      newErrors.password = '비밀번호를 입력하세요';
      passwordRef.current.focus();
    }

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }

    console.log('로그인 성공!', form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          ref={usernameRef}
          name="username"
          value={form.username}
          onChange={handleChange}
          placeholder="아이디"
          style={{ borderColor: errors.username ? 'red' : '#ddd' }}
        />
        {errors.username && <span style={{ color: 'red' }}>{errors.username}</span>}
      </div>

      <div>
        <input
          ref={passwordRef}
          name="password"
          type="password"
          value={form.password}
          onChange={handleChange}
          placeholder="비밀번호"
          style={{ borderColor: errors.password ? 'red' : '#ddd' }}
        />
        {errors.password && <span style={{ color: 'red' }}>{errors.password}</span>}
      </div>

      <button type="submit">로그인</button>
    </form>
  );
}

실전 예제: 캔버스 그리기

import { useRef, useEffect, useState } from 'react';

function DrawingCanvas() {
  const canvasRef = useRef(null);
  const [isDrawing, setIsDrawing] = useState(false);
  const lastPos = useRef({ x: 0, y: 0 });

  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
  }, []);

  const startDrawing = (e) => {
    const canvas = canvasRef.current;
    const rect = canvas.getBoundingClientRect();
    lastPos.current = {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    };
    setIsDrawing(true);
  };

  const draw = (e) => {
    if (!isDrawing) return;

    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    const rect = canvas.getBoundingClientRect();

    const currentPos = {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    };

    ctx.beginPath();
    ctx.moveTo(lastPos.current.x, lastPos.current.y);
    ctx.lineTo(currentPos.x, currentPos.y);
    ctx.stroke();

    lastPos.current = currentPos;
  };

  const stopDrawing = () => {
    setIsDrawing(false);
  };

  const clearCanvas = () => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  };

  return (
    <div>
      <canvas
        ref={canvasRef}
        width={400}
        height={300}
        style={{ border: '2px solid #333', cursor: 'crosshair' }}
        onMouseDown={startDrawing}
        onMouseMove={draw}
        onMouseUp={stopDrawing}
        onMouseLeave={stopDrawing}
      />
      <br />
      <button onClick={clearCanvas}>지우기</button>
    </div>
  );
}

실전 예제: 무한 스크롤 감지

import { useRef, useEffect, useState } from 'react';

function InfiniteScroll() {
  const [items, setItems] = useState(Array.from({ length: 20 }, (_, i) => i + 1));
  const [loading, setLoading] = useState(false);
  const observerRef = useRef(null);

  const loadMore = () => {
    setLoading(true);
    // 데이터 로딩 시뮬레이션
    setTimeout(() => {
      setItems((prev) => [
        ...prev,
        ...Array.from({ length: 10 }, (_, i) => prev.length + i + 1)
      ]);
      setLoading(false);
    }, 1000);
  };

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && !loading) {
          loadMore();
        }
      },
      { threshold: 1.0 }
    );

    if (observerRef.current) {
      observer.observe(observerRef.current);
    }

    return () => observer.disconnect();
  }, [loading]);

  return (
    <div style={{ height: '400px', overflow: 'auto' }}>
      {items.map((item) => (
        <div
          key={item}
          style={{
            padding: '20px',
            borderBottom: '1px solid #ddd'
          }}
        >
          아이템 {item}
        </div>
      ))}
      <div ref={observerRef} style={{ padding: '20px', textAlign: 'center' }}>
        {loading ? '로딩 중...' : '더 보기'}
      </div>
    </div>
  );
}

useRef 사용 시 주의사항

1. 렌더링 중에 current를 읽거나 쓰지 마세요

// ❌ 안 좋음
function BadComponent() {
  const ref = useRef(0);
  ref.current += 1;  // 렌더링 중에 변경!
  return <p>{ref.current}</p>;  // 렌더링 중에 읽기!
}

// ⭕ 좋음 - 이벤트 핸들러나 useEffect 안에서 사용
function GoodComponent() {
  const ref = useRef(0);

  const handleClick = () => {
    ref.current += 1;
    console.log(ref.current);
  };

  return <button onClick={handleClick}>클릭</button>;
}

2. DOM ref는 null일 수 있음

function SafeRef() {
  const divRef = useRef(null);

  const handleClick = () => {
    // null 체크!
    if (divRef.current) {
      divRef.current.style.backgroundColor = 'red';
    }
  };

  return <div ref={divRef}>내용</div>;
}

3. 화면에 보여줄 값은 useState를 사용하세요

// ❌ 화면이 안 바뀜
const count = useRef(0);
return <p>{count.current}</p>;  // 클릭해도 화면 그대로

// ⭕ 화면이 바뀜
const [count, setCount] = useState(0);
return <p>{count}</p>;  // 클릭하면 화면 업데이트

useRef vs useState 정리

상황사용할 Hook
화면에 보여줘야 함useState
DOM 요소 접근useRef
화면 업데이트 없이 값 저장useRef
타이머/인터벌 ID 저장useRef
이전 값 기억useRef

다음 단계

useRef를 배웠으니, 다음 글에서는 React의 Hooks 전체를 정리해볼게요. useState, useRef, useEffect, useMemo, useCallback 등 주요 Hooks를 한눈에 볼 수 있게 정리해드릴게요.

useRef는 처음엔 언제 써야 할지 애매한데, 몇 번 써보면 “아, 이럴 때 쓰는 거구나” 감이 와요!


React 시리즈 탐색:

← 블로그 목록으로