useRef로 React에서 DOM 직접 다루기

Kingmo·2022년 4월 9일
post-thumbnail

순수 자바스크립트에서는 특정 태그를 조작할 때 document.getElementById나 querySelector 같은 DOM API를 직접 호출했다.

하지만 React는 가상 DOM을 기반으로 선언적 렌더링을 수행하기 때문에, 개발자가 브라우저 DOM을 직접 수정하는 것은 지양해야 한다. 그럼에도 불구하고 실제 DOM 노드에 직접 접근해야 하는 순간이 존재하며, 이때 사용하는 훅이 바로 useRef다.

useRef를 사용하는 대표적인 상황

React의 선언적 상태 관리만으로 처리하기 힘든 명령형 작업에 사용한다.

  • 특정 입력 필드에 자동으로 focus를 주거나 해제할 때
  • 스크롤 위치를 감지하거나 특정 위치로 스크롤을 이동시킬 때
  • 특정 DOM 엘리먼트의 크기(offsetWidth, offsetHeight)나 화면상 위치(getBoundingClientRect)를 계산할 때
  • Canvas API, D3, 지도 SDK 등 순수 DOM 노드를 반드시 넘겨받아야 하는 외부 라이브러리를 연동할 때

기본 문법과 동작 원리

useRef는 { current: initialValue } 형태의 변경 가능한 객체를 반환한다.

import { useRef } from "react";

export default function TextInput() {
  // 1. ref 객체 생성 (초기값 null)
  const inputRef = useRef(null);

  const handleFocus = () => {
    // 3. ref.current를 통해 실제 DOM 노드에 접근 후 DOM API 호출
    inputRef.current?.focus();
  };

  // 2. 접근하고자 하는 JSX 엘리먼트에 ref 속성으로 연결
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>입력창으로 포커스 이동</button>
    </div>
  );
}
  • React가 컴포넌트를 마운트하고 실제 DOM 노드를 생성하면, inputRef.current 프로퍼티에 해당 DOM 엘리먼트의 참조값이 할당된다.
  • 컴포넌트가 언마운트되면 current 값은 다시 null로 정리된다.

실무 응용: 숨겨진 파일 인풋(File Input) 제어

기본 브라우저의 <input type="file" />은 디자인을 커스텀하기 매우 까다롭다. 실무에서는 파일 인풋을 화면에서 숨겨두고, 별도로 디자인된 버튼이나 이미지를 클릭했을 때 인풋의 파일 탐색기를 대신 띄우는 패턴을 많이 사용한다.

import { useRef } from "react";

export default function FileUpload() {
  const fileInputRef = useRef(null);

  // 버튼이나 이미지를 눌렀을 때 숨겨진 file input을 대신 클릭해 주는 핸들러
  const handleFileTrigger = () => {
    fileInputRef.current?.click();
  };

  const handleFileChange = (event) => {
    const file = event.target.files?.[0];
    if (file) {
      console.log("선택된 파일:", file.name);
    }
  };

  return (
    <div>
      {/* 1. 실제 파일 인풋은 화면에서 숨김 처리 */}
      <input
        ref={fileInputRef}
        type="file"
        hidden
        onChange={handleFileChange}
      />

      {/* 2. 클릭 트리거 역할을 할 커스텀 UI */}
      <img
        src="/preview-placeholder.png"
        alt="업로드 미리보기"
        onClick={handleFileTrigger}
        style={{ width: "200px", cursor: "pointer" }}
      />
      <button type="button" onClick={handleFileTrigger}>
        이미지 등록하기
      </button>
    </div>
  );
}
  • fileInputRef.current.click()을 호출하면 사용자가 실제 숨겨진 <input type="file" />을 누른 것과 완전히 동일하게 파일 탐색기 창이 열린다.
  • 이를 통해 브라우저 기본 UI에 구애받지 않고 원하는 형태의 업로드 UI를 구현할 수 있다.

주의: useRef와 useState의 결정적 차이

useRef는 DOM 참조뿐만 아니라 렌더링을 유발하지 않는 컴포넌트 내부 변수를 저장할 때도 사용된다.

  • useState: 값이 변경되면 컴포넌트가 리렌더링된다. 화면에 직접 표시되는 UI 데이터에 사용한다.
  • useRef: ref.current 값이 바뀌어도 컴포넌트는 리렌더링되지 않는다.
    • 컴포넌트의 생애주기 동안 값은 유지하되, 화면 갱신과 무관한 값(setTimeout 타이머 ID, 이전 렌더링 값 추적 등)을 저장할 때 적합하다.
profile
Developer

1개의 댓글

comment-user-thumbnail
2023년 9월 22일

감사합니다 ㅎㅎ이해가잘되었어용

답글 달기