[멋쟁이사자처럼 부트캠프 프론트엔드 14기/REACT] Value & DOM Ref

minji·2025년 8월 30일
post-thumbnail

"나는야 1인 2역! 값도 참조하고, DOM도 조작해!"

역할 1. 값 참조

useRef 훅 함수 개념

| 리액트에서 렌더링 사이 값을 저장할 수 있게 해주는 훅으로 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는데 적합하다.

useRef 의 몇 가지 특징을 살펴보면 다음과 같다.

  • 일반적으론 반응성 데이터인 state, props 가 변경되면 리렌더링이 일어나지만,
    useRef 훅을 사용하면 반응성 데이터(state, props) 값이 변경되어도 컴포넌트 리렌더링이 일어나지 않는다. ⇒ "렌더 트리거"가 없다.
  • 기본공식은 const ref = useRef(initialValue) 이며, 사용 형태는 다음과 같다.
// 컴포넌트 최상위 레벨에서 호출하여 선언
import { useRef } from 'react'

export default function App() {
	const countRef = useRef(1)

	return (...
}
  • 초기설정값initialValue 는 초기 렌더링이후에는 무시된다.
  • 반환값으로는 순수한 자바스크립트 객체인 ref 객체 가 반환되는데, {current: 값} 형태이며 ref.current 로 값 읽기/수정 또는 접근할 수 있다.
  • 컴포넌트마다 독립적으로 자신만의 ref 를 가지며, 서로의 ref 객체를 공유하지는 않는다. (like 클래스 컴포넌트 인스턴스 멤버 작동)
  • 한 컴포넌트에서 하나 이상의 ref 를 가질 수 있다. (캐러셀 구현시 해당됨)

💡 참고사항

  • 사용자 정의 훅 내에서 리액트의 빌트인(내장) 훅 사용이 가능하다.

이런 특징들을 가진 useRef 훅 함수는 어떤 상황에서 사용하는 것이 효과적일까?
1️⃣ 이전 값을 어딘가 기억해둬야할 때 useRef 훅 함수가 빛을 발한다.

이전 값을 기억할 때, 다음 두 가지 사항을 참고해야한다.

  • 리액트 훅의 규칙에서도 살펴봤지만, 컴포넌트 외부 에 상태값을 기억하면 안된다.
  • 이전 상태값 기억을 위해서는 렌더링 이후 실행되는 이펙트 함수 를 사용하여 값을 참조해야한다.

2️⃣ 타이머 기능이 예시로 가장 적합하지 않을까싶다.

타이머 기능 구현시, setInterval 함수가 반환한 타이머ID 값은 렌더링에 영향을 받지않도록 기억해둬야하는데 이럴 때 ref 객체를 생성하여 저장해둬야 한다.

import { useState, useRef } from 'react'

export default function App() {
  const [timer, setTimer] = useState(null)
  // useRef 훅함수 생성
  const timerIdRef = useRef()

  const handleStartTimer = () => {
    ;(function recursion() {
      clearTimeout(timerIdRef.current)
      timerIdRef.current = setTimeout(() => {
        console.log('타이머 작동 중...')
        setTimer(new Date())
        recursion()
      }, 1000)
    })()
  }

  const handleStopTimer = () => {
    console.log('타이머 중지!')
    clearTimeout(timerIdRef.current)
    setTimer(null)
  }

  return (
    <section className="app">
      <h1>타이머</h1>
      <div role="group">
        <button type="button" onClick={handleStartTimer}>
          타이머 시작
        </button>
        <button type="button" onClick={handleStopTimer}>
          타이머 중지
        </button>
      </div>
      {timer && (
        <time dateTime={timer.toISOString()}>
	        {timer.toLocaleTimeString()}
	      </time>
      )}
    </section>
  )
}

클래스 컴포넌트 vs. 함수형 컴포넌트

클래스 컴포넌트는 자체적으로 상태관리가 가능하지만, 함수형 컴포넌트는리액트 훅을 사용하여 자체적으로 상태관리가 가능하다.

그렇다면 useRef 훅을 다뤄보는 이 시점에서 두 컴포넌트의 차이점에는 어떤 점이 있는지 살펴보고 넘어가자.

클래스 컴포넌트

  • 버튼을 클릭할 때마다 리렌더링이 일어나지 않는다.❌
    → 이유는 메서드 내부의 매개변수 자체만 변경되어있고, 컴포넌트의 상태는 변경되어있지 않기 때문이다.
    (리렌더링을 유발하는 것은 상태변경이다!)
  • render 메서드를 재실행시킬 때마다 초기화가 이루어지기 때문에 이전 값을 기억할 수 없다.
    but, 클래스 필드에 인스턴스 멤버로 생성을 하면 렌더링될 때마다 값을 기억하고 변경할 수 있다.
  • 렌더링을 트리거하려면 render() 내부에 setState 를 꼭 사용해야한다.
  • 클래스 컴포넌트는 상태(state) 나 속성(props)이 아니더라도 특정한 값을 기억하는 것이 가능하다.
    (클래스 내부의 인스턴스 변수인 this 는 고유한 메모리 공간을 가지므로, this를 통해 선언된 변수들은 인스턴스가 유지되는 동안 값을 저장할 수 있다)
  • render 외부의 값을 기억할 수 있다. → 왜? render 의 외부일뿐, 클래스 컴포넌트의 내부니까!

💡 리액트의 반응성 데이터에는 어떤 것들이 있을까?

  • 상태 / 속성 / 컨텍스트 / 파생된 상태

함수형 컴포넌트

  • 렌더링을 한다는 것은 함수를 재실행 시키는 것이다. → 지역변수는 렌더링될 때마다 값이 초기화되어 값을 기억할 수 없다.
  • 함수 몸체는 값을 기억을 할 수 없다.
    함수 외부에 위치 시키면? "No!" 외부의 값을 참조하는 형태가되어 순수함이 사라지며,(불순) 예측불가능한 상황이 생긴다.
    → *값을 기억하고 싶을 땐? 함수형 컴포넌트 + 메모이제이션 공식을 이용하면 값 참조가 가능하다.
    ⇒ 이 기능을 하는 것이 바로 리액트 훅 함수 이다.

리액트는 값을 기억할 수 있게 만들었다! → useRef 를 이용해라!


역할 2. DOM 조작

렌더링은 대부분 리액트에 의해 제어되지만, 모든 경우를 제어하는 것은 불가능하다.

지금부터 다뤄볼 DOM조작 관련한 내용이 리액트가 제어할 수 없는 것들 중 하나이다.
실제 DOM 노드에 접근 또는 조작하는 것은 리액트의 렌더링과는 무관한 일이기도하고, 리액트가 렌더링 결과물에 맞춰 자동으로 DOM변경을 처리하기 때문에 컴포넌트에서 직접적으로 조작할 일이 드물긴하다.

그래도 가끔씩 DOM요소에 접근해야하는 경우들이 있는데, 대표적인 상황들은 다음과 같다.

  • 포커스(초점) 제어
  • 스크롤 제어
  • 위치/크기 조정
  • 미디어 및 기타 브라우저 API 사용
    • cavas 제어 (drawing 제어)
    • vedio/audio 제어 (재생/정지 제어)
  • 라이브러리를 적용을 위한 제어
    • Canvas Confetti (폭죽효과)
    • Vanilla Tilt (틸트(기울기) 효과)
    • GSAP (애니메이션 효과) → 단, GSAP은 리액트 전용훅으로 useGSAP 을 제공하므로 훅 사용권장

DOM 노드에 접근하고 조작하기

접근순서

1️⃣ useRef 훅 가져오기

import { useRef } from 'react'

2️⃣ ref 선언을 위해 컴포넌트 내부에서 가져온 훅 사용하기

const myRef = useRef(null)

3️⃣ DOM노드를 가져와야하는 JSX tag 에 ref 를 attribute로 전달하기

<div ref={myRef}>

접근을 하고나면, 리액트가 해당(<div>) DOM노드를 생성할 때, 이 노드에 대한 참조를 초기값이 null 이였던 myRef.current 에 넣는다.
그럼 이제 이 DOM노드를 이벤트 핸들러에서 접근하거나 노드에 정의된 내장 브라우저 API를 사용할 수 있게 된다.

// 예시 - 아래와같이 브라우저 API를 사용할 수 있다.
myRef.current.scrollIntoView()

💬 Ref로 DOM 접근 및 조작하기 관련 참고내용

  • useRef는 리액트 앨리먼트가 실제 DOM에 마운트된 이후 참조할 수 있다.
  • render 단계에서 DOM요소에 접근하려고하면 null 이 반환된다.
  • 실제 DOM Node에 접근을 원할 경우, useEffect 를 사용해야 할 수도 있다.
    → 왜냐면 마운트 이후에 useEffect 가 실행되기 때문이다.
  • 참조 콜백(ref callback) 안에서 useEffect 실행도 가능하다.

커스텀 컴포넌트의 DOM 노드에 접근하기

브라우저 요소를 출력하는 내장 컴포넌트(<input />)에 ref를 부여하면 리액트는 ref 의 current 프로퍼티를 브라우저의 실제 <input /> 과 같은 노드로 설정한다.

하지만, <MyInput /> 처럼 직접 만든 커스텀 컴포넌트에 ref를 부여하면 기본값으로 null 이 주어져 버튼을 클릭해도 input 요소에 focus 되지 않는다.

그렇다면 focus 되게하려면 어떻게 해야할까?
ref는 prop 처럼 부모 컴포넌트에서 자식 컴포넌트로 전달을 할 수가 있다.

import { useRef } from 'react';

function MyInput({ ref }) {
  return <input ref={ref} />;
}

function MyForm() {
  const inputRef = useRef(null);
  return <MyInput ref={inputRef} />
}

위 예시에서 이를 적용시키면, 부모 컴포넌트인 MyForm 에서 ref를 생성하고 이를 자식 컴포넌트인 MyInput으로 전달한다. 그리고 MyInput 은 전달받은 ref를 반환하는 내장 컴포넌트인 <input> 에 전달하게 되므로 ref의 current<input> DOM요소로 설정이 된다.

이 방식으로 진행하면 커스텀 컴포넌트에서도 ref를 사용할 수 있다.

💬 커스텀 컴포넌트 DOM노드 접근관련 참고내용

  • 첫 렌더링에서는 DOM노드가 아직 생성되지 않았기 때문에 ref.currentnull 인 상태이다.
  • 리액트는 ref.current커밋 단계에서 설정한다.
  • DOM변경 후 리액트는 대응하는 DOM노드로 ref.current 를 재설정한다.
  • 대부분 ref 접근은 이벤트 핸들러 안에서 일어나는데, ref를 사용하여 특정 동작을 하고 싶지만 시행할 이벤트가 없을 땐 Effect 가 필요할 수 있다.

함수 내부에서 특정한 값을 기억하고 싶은데, 렌더링은 유발하고싶지않을 때 useRef 를 사용하면 좋다.


"Ref 는 리액트가 추적하지 않는 구성요소의 비밀 주머니라고 할 수 있다. 이것이 리액트의 단방향 데이터 흐름에서 탈출구가 되는 것이다."


ref 콜백이란?

리액트 규칙 글에서 살펴봤듯이 리액트 훅의 규칙에는 다음과 같은 내용이 있다.

  • 훅은 컴포넌트 최상단 에서만 호출되어야 한다.
  • 훅은 반복문, 조건문, map()에서 호출할 수 없다. (이 글에서 map() 에서도 호출할 수 없음을 추가하겠다.)

반복문이나 map() 에서 사용할 수 없는 상황을 해결하는 방법에 ref 콜백 이 있다.
ref callbackref attribute함수를 전달하는 것이다.

리액트19에서는 ref를 설정할때는 DOM노드와 함께 ref콜백을 호출하고, ref를 지울 때 ref콜백이 클린업함수(정리함수)를 반환하지 않으면, null로 호출된다고 했으나, 해당 동작은 향후 버전에서 제거될 예정인 점 참고하자.


💡추가로 알아두면 좋을 내용

Ref는 State처럼 숫자뿐만아니라 문자열, 객체, 함수 등 모든 것을 가리킬 수 있다.
또한, State와 마찬가지로 리렌더에 의해 유지된다.
하지만 State는 변경하면 컴포넌트 리렌더링이 발생하지만, Ref는 변경해도 리렌더링이 발생하지 않는다.

이렇듯 Ref 와 State 는 비슷한 면도 있지만 명확한 차이점이 있다.
이와 관련하여 리액트의 공식문서에 나와있는 Ref 와 State의 차이를 다음표를 통해 살펴보자.

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

🔗 참고자료
https://ko.react.dev/reference/react/useRef
https://ko.react.dev/learn/manipulating-the-dom-with-refs
https://ko.react.dev/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback
https://ko.react.dev/learn/referencing-values-with-refs

profile
FE Developer .mjs

0개의 댓글