리액트 useRef 사용법: DOM 제어, input focus, 이전 값 저장하기

리액트 useRef 사용법: DOM 제어, input focus, 이전 값 저장하기

useRef는 렌더링 사이에 값을 보존하거나, 실제 DOM 요소에 직접 접근해 포커스·스크롤·스타일을 제어할 때 쓰는 훅입니다. ref.current 값을 바꿔도 리렌더링은 일어나지 않으므로 화면에 보여줄 값은 여전히 useState로 관리하는 것이 원칙입니다.

1. useRef 핵심 요약

  • 형태: const elRef = useRef(null)elRef.current에 DOM 노드(또는 임의의 값) 저장
  • 리렌더 X: ref.current 변경은 리렌더를 트리거하지 않음(성능 이점)
  • 주요 용도: 포커스/스크롤/선택 제어, 외부 라이브러리 핸들 보관, 이전 값/타이머 ID 저장

2. 예제 1 — DOM 직접 제어(스크롤·하이라이트)

버튼을 클릭하면 지정 박스로 스무스 스크롤되고, 테두리가 잠깐 하이라이트됩니다. ref.current를 통해 스타일을 직접 변경합니다.

import React, { useRef } from 'react';

function DomControlDemo() {
  const boxRef = useRef(null);

  const scrollToBox = () => {
    boxRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
  };

  const highlight = () => {
    const el = boxRef.current;
    if (!el) return;
    el.style.transition = 'box-shadow 300ms';
    el.style.boxShadow = '0 0 0 3px #3b82f6 inset';
    setTimeout(() => { if (el) el.style.boxShadow = 'none'; }, 700);
  };

  return (
    <section style={{border:'1px solid #ddd', padding:12, borderRadius:8, height:600, overflowY:'auto'}}>
      <h4>DomControlDemo 예제</h4>
      <p>버튼으로 대상 박스로 스크롤하고, 테두리를 잠시 하이라이트합니다.</p>

      <div style={{display:'flex', gap:8, marginBottom:12}}>
        <button onClick={scrollToBox}>해당 영역으로 스크롤</button>
        <button onClick={highlight}>하이라이트</button>
      </div>

      {/* 스크롤 공간용 플레이스홀더 */}
      {Array.from({ length: 4 }).map((_, i) => <div key={i} style={{ height: 80 }} />)}

      <div
        ref={boxRef}
        style={{
          height:160,
          background:'#f8fafc',
          border:'1px solid #94a3b8',
          borderRadius:8,
          display:'grid',
          placeItems:'center',
          fontWeight:'bold'
        }}
      >
        타깃 상자 (ref 대상)
      </div>

      {Array.from({ length: 8 }).map((_, i) => <div key={i} style={{ height: 80 }} />)}
    </section>
  );
}

export default DomControlDemo;

▼ 실행 화면 설명: 스크롤 컨테이너 상단의 버튼을 누르면 화면이 부드럽게 아래 ‘타깃 상자’로 이동합니다. “하이라이트”를 누르면 상자 내부 테두리가 잠시 파란색으로 반짝입니다.

리액트 useRef DOM 직접 제어(스크롤·하이라이트) 예제 실행 결과 화면

3. 예제 2 — input focus 제어(자동 포커스·Enter로 다음 이동)

첫 렌더링 시 이름 입력란에 자동 포커스가 가고, Enter를 누르면 다음 입력으로 포커스가 이동합니다.

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

function InputFocusDemo() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);

  // 마운트 시 첫 입력창 자동 포커스
  useEffect(() => {
    nameRef.current?.focus();
  }, []);

  const focusNext = () => emailRef.current?.focus();

  return (
    <section style={{border:'1px solid #ddd', padding:12, borderRadius:8}}>
      <h4>InputFocusDemo 예제</h4>
      <p>첫 렌더링 시 이름 입력창에 포커스가 자동으로 이동하고, Enter 입력 시 이메일 입력창으로 넘어갑니다.</p>

      <label>이름</label>
      <input
        ref={nameRef}
        placeholder="이름 입력"
        onKeyDown={(e) => { if (e.key === 'Enter') focusNext(); }}
        style={{display:'block', width:'100%', marginBottom:8}}
      />

      <label>이메일</label>
      <input
        ref={emailRef}
        placeholder="email@example.com"
        style={{display:'block', width:'100%', marginBottom:8}}
      />

      <button onClick={focusNext}>다음 입력으로 포커스</button>

    </section>
  );
}

export default InputFocusDemo;

▼ 실행 화면 설명: 컴포넌트가 나타나자마자 이름 입력창 커서가 깜박입니다. 이름 입력 후 Enter를 누르거나 버튼을 클릭하면 이메일 입력창으로 포커스가 이동합니다.

리액트 useRef input focus 제어(자동 포커스·Enter로 다음 이동) 예제 코드 실행 화면

4. 예제 3 — 이전 값 저장(렌더 간 값 비교)

입력할 때마다 이전 렌더에서의 값useRef에 저장해 현재 값과 나란히 보여줍니다. 상태(State)는 화면 표시용, ref는 비교·기록용으로 분리합니다.

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

function PreviousValueDemo() {
  const [text, setText] = useState('');
  const prevTextRef = useRef(text);   // 초기값을 현재 값으로 저장

  // text가 바뀔 때마다 '이전 값 저장소' 갱신
  useEffect(() => {
    prevTextRef.current = text;
  }, [text]);

  // 리렌더 없이 누적 카운트 저장 (UI 보조 정보용)
  const changeCountRef = useRef(0);
  useEffect(() => {
    changeCountRef.current += 1;
  }, [text]);

  return (
    <section style={{border:'1px solid #ddd', padding:12, borderRadius:8}}>
      <h4>PreviousValueDemo 예제</h4>
      <p>입력할 때마다 현재 값과 이전 값을 나란히 보여주고, 변경 횟수는 ref에 누적됩니다.</p>

      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="아무 내용이나 입력"
        style={{display:'block', width:'100%', marginBottom:12}}
      />

      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:12}}>
        <div style={{background:'#f8fafc', padding:8, borderRadius:6}}>
          <strong>현재 값</strong>
          <pre style={{margin:0}}>{text || '(빈 문자열)'}</pre>
        </div>
        <div style={{background:'#fff7ed', padding:8, borderRadius:6}}>
          <strong>이전 값 (ref)</strong>
          <pre style={{margin:0}}>{prevTextRef.current || '(초기값)'}</pre>
        </div>
      </div>

      <p style={{marginTop:8, color:'#64748b'}}>
        변경 횟수(리렌더 없이 ref로 누적): {changeCountRef.current}
      </p>

      <hr/>
      <pre style={{background:'#f6f8fa', padding:8}}>
        {JSON.stringify(
          { current: text, previous: prevTextRef.current, changes: changeCountRef.current },
          null,
          2
        )}
      </pre>
    </section>
  );
}

export default PreviousValueDemo;

▼ 실행 화면 설명: 입력 상자 아래 ‘현재 값’과 ‘이전 값’이 나란히 표시됩니다. 타이핑할 때마다 이전 렌더의 값을 정확히 비교할 수 있습니다. 변경 횟수는 ref로 누적되며, 이 값 자체는 리렌더를 유발하지 않습니다.

리액트 useRef 이전 값 저장(렌더 간 값 비교) 예제 코드 실행 화면

5. 자주 하는 실수 & 베스트 프랙티스

  • 화면 표시값은 state로: ref.current 변경만으로는 화면이 갱신되지 않습니다. UI에 보여줄 데이터는 useState.
  • DOM 조작은 최소화: 가능하면 선언적 방식(조건부 렌더링·스타일 바인딩) 우선, 꼭 필요할 때만 ref로 직접 제어.
  • 이벤트 리스너/외부 핸들 보관: 등록한 핸들러/타이머 ID/외부 인스턴스는 ref에 보관하고, 정리는 useEffect의 cleanup에서 수행.
  • Strict Mode 이중 호출 이슈: 개발 모드에서 효과/마운트가 두 번처럼 보일 수 있으나 ref 저장 자체는 문제 없습니다(React 18 개발모드 특성).

참고 링크

함께 보면 좋은 게시글

위로 스크롤