3 분 소요

useRef로 값 참조하기

const ref = useRef(initialValue);

리액트에서 Ref 사용을 권장하는 상황

1. ✅ 직접 DOM에 접근해야 할 때

리액트에서 직접적으로 DOM 조작을 하는 내장 메서드를 제공하지 않기 때문에 직접 특정 노드 포커스 조작, 스크롤 이동, 위치와 크기를 측정 등 직접 DOM에 접근해야 할 때 사용

2. ✅ 렌더링이 필요 없는 값들을 저장해야 할 때

ref는 일반 JavaScript 객체이기 때문에 ref값이 변경돼도 React는 사용자가 언제 값을 변경했는지 알지 못하기 때문에 렌더링을 하지 않는다.

3. ✅ 브라우저 내장 API를 호출해야 할 때

dialog 요소는 브라우저가 제공하는 메서드인 showModal()로만 모달을 열 수 있다.
React의 상태로 대신할 수 있는 게 아니기 때문에 직접 호출이 필요하다.

dialog.current.showModal(); // 브라우저 API

🤔 Web API(웹 플랫폼): 자바스크립트가 브라우저와 상호작용할 수 있도록 브라우저에서 제공하는 기능들

4. ✅ setTimeout Timer ID 같은 React 외부 요소를 저장해야 할 때

timer ID는 React나 JS 엔진이 아닌 브라우저가 내부적으로 관리하는 ID 값으로 React가 타이머 ID를 모르기 때문에 자동으로 관리할 수 없다.
🤔 React 외부 요소: React의 상태나 렌더링과는 관계없이 수동으로 생성/정리 필요한 것들


참조를 사용해 DOM 조작하기

useRef는 { current : 값 } 형태의 객체(object)를 반환한다.

import { useRef } from 'react';

export default Parent(){
const inputRef = useRef("");//컴포넌트 내에서 useRef Hook을 호출하고 참조할 초깃값을 인자로 전달

const handleSettingInput = () => {
  setEnteredInput(inputRef.current.value);
  inputRef.current.value = "" // 직접 DOM을 조작(명령형) 참조를 사용하는 목적이 명확할 때 사용
}

return (
 <>
  <input ref={inputRef}/> //ref.current 프로퍼티를 통해 해당 ref의 current 값에 접근
  <button type="button" onClick={handleSettingInput}>SetName</button>
 </>
  )
}

커스텀 컴포넌트로 Ref(참조) 전달

ref를 특정 컴포넌트에 전달하면, 해당 요소의 DOM 요소를 직접 참조할 수 있다.

⚠️ 여러 개의 같은 Dialog 컴포넌트가 있을 경우 refs가 어떤 Dialog 요소와 연결되는지 명확하지 않으면 예상치 못한 동작이 발생할 수 있다.

export default Parent(){
    const dialogRef = useRef(null);
    return <Modal dialogRef={dialogRef} result={result}/>
}

//Modal props 로 dialogRef 전달
export default ResultModal({result, dialogRef}){
    return(
    //ref 프로퍼티는 리액트 모든 내장 컴포넌트에서 지원
    <dialog ref={dialogRef}>
      <div>{result}</div>
      <form method="dialog">
        <button>닫기</button>
      </form>
    </dialog>
    )
}

⚠️ React 19 부터 ref를 props로 받을 수 있음! React 18 또는 그 이전 버전에서는 ref를 전달하기위해 forwardRef 를 사용

import { forwardRef } from "react";

const ResultModal = forwardRef(function ResultModal({ result }, dialogRef) {
  //두 번째 파라미터로 전달
  return (
    <dialog ref={dialogRef}>
      <div>{result}</div>
      <form method="dialog">
        <button>닫기</button>
      </form>
    </dialog>
  );
});
export default ResultModal;

🔗 forwardRef React

useRef (참조)와 useState(상태)의 차이

useRef 참조 state 상태
useRef 값이 변경되어도 컴포넌트 리렌더링 ❌ 상태가 변경되면 컴포넌트 재실행 ⭕️
UI에 영향을 끼치지 않는 값들을 가지고 있는 경우 사용 UI에 바로 반영되야하는 값들이 있을 때 사용
DOM 요소에 직접적인 접근, 조작이 필요할 때 사용 어떠한 종류의 값이든 상태로 관리 가능
컴포넌트가 리렌더링되어도 초기화되지 않는다 특정 조건에서 초기화 될 수 있다

🔗 useRef React

useImperativeHandle

자식 컴포넌트에서 사용자가 직접 정의한 함수나 값을 외부에 노출하고 싶을 때 사용하는 훅

useImperativeHandle(ref, createHandle, dependencies?)

부모에서 자식의 ref 를 직접 DOM에 연결할 경우 문제점

부모가 자식의 실제 DOM 요소와 메서드에 직접 접근, 조작이 가능해진다.

// 부모
const dialogRef = useRef();
dialogRef.current.showModal(); // 직접 DOM 조작
<Modal ref={dialogRef} />
//ref.current를 통해 자식 안의 dialog에 직접적으로 접근

// 자식
<dialog ref={ref}>내용</dialog>

🤔 리액트에서 DOM의 직접 조작이 문제가 되는 이유는 ?

상태와 UI 사이의 일관성이 깨지기 때문, 리액트에서는 상태가 변경되면 그에 따라 자동으로 UI가 변경되는 구조를 따르는데, React의 상태를 건드리지 않고 DOM만 바꾸는 방식이기 때문에 React는 UI가 바뀌었는지 모르게 된다.

부모가 자식의 구현에 의존하는 형태로 결합도(coupling)가 높아져 유지보수가 어려워진다

자식 컴포넌트를 수정하면 부모도 강제 수정이 필요해진다.
자식이 독립적인 컴포넌트로 재사용되기 힘들어진다.

//부모
dialogRef.current.showModal(); //<div>에는 showModal이 없기 때문에 에러 발생

//자식
export default function Modal({ ref }) {
  return <div ref={ref}>내용</div>;
}

📌 useImperativeHandle 을 사용한 해결 방법

✅ 부모 → 자식에게 전달하는 ref의 역할: 자식 컴포넌트의 open 메서드에 접근하기 위한 ref
✅ useImperativeHandle를 통해 자식이 부모에게 필요한 메서드만 외부에 명시적으로 노출
✅ 자식 컴포넌트에서 생성한 ref의 역할: DOM 요소인 dialog 태그에 접근하기 위한 ref, 내부 DOM을 직접 제어(다중 참조로 인한 충돌 해결을 위해 각각의 자식 컴포넌트에 개별적인 ref 할당)

✅ 결론적으로 자식의 내부 구현(dialog, showModal)이 바뀌더라도, open() 함수 내부만 고치면 된다.
자식 컴포넌트가 다른 구조로 바뀌더라도 부모는 변경 없이 재사용이 가능해진다!

//부모 컴포넌트
export default function Parent() {
  const dialogRef = useRef(); // 모달 컨트롤을 위한 ref

  return (
    <>
      <button onClick={() => dialogRef.current?.open()}>모달 열기</button>
      <ResultModal dialogRef={dialogRef} result={result}/>
    </>
  );
}


//자식 컴포넌트
import { useImperativeHandle, useRef } from "react";

export default function ResultModal({ dialogRef, result}) {
  const dialog = useRef(); //dialog 요소에 접근하기위한 별도의 ref

  useImperativeHandle(dialogRef, handleOpen); //부모가 사용할 수 있도록 외부로 노출

  function handleOpen () {
    return {
    open() { //dialog.current.showModal()을 감싼 open() 메서드 정의
	dialog.current.showModal();
    onClose() {
        dialog.current.onReset();
    },
  } };

  return (
  //ref의 프로퍼티 값으로 dialog 전달
    <dialog ref={dialog} onClose={handleReset}>
        <div>{result}</div>
        <form method="dialog">
            <button>닫기</button>
        </form>
    </dialog>
  );
}

🔗 useImperativeHandle React

댓글남기기