levi

리바이's Tech Blog

Tech BlogPortfolioBoard
AllActivitiesJavascriptTypeScriptNetworkNext.jsReactWoowacourseAlgorithm
COPYRIGHT ⓒ eunwoo-levi
eunwoo1341@gmail.com

📚 목차

    [React 공식문서] useRef를 활용한 DOM 접근 (부록: useImperativeHandle)

    ByEunwoo
    2025년 5월 5일
    react

    useRef란?

    useRef는 렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook이다.

    const ref = useRef(initialValue);

    컴포넌트의 최상위 레벨에서 useRef를 호출하여 ref를 선언한다.

    import { useRef } from 'react';
     
    function MyComponent() {
      const intervalRef = useRef(0);
      const inputRef = useRef(null);
      // ...

    매개변수

    initialValue: ref 객체의 current프로퍼티 초기 설정값이다.
    여기에는 어떤 유형의 값이든 지정할 수 있다. 이 인자는 초기 렌더링 이후부터는 무시된다.

    반환값

    useRef는 단일 프로퍼티를 가진 객체를 반환한다.

    current: 처음에는 전달한 initialValue로 설정된다. 나중에 다른 값으로 바꿀 수 있다. ref 객체를 JSX 노드의 ref 어트리뷰트로 React에 전달하면 React는 current프로퍼티를 설정한다.
    다음 렌더링에서 useRef는 동일한 객체를 반환한다.

    주의사항

    • ref.current 프로퍼티는 state와 달리 변이할 수 있다. 그러나 렌더링에 사용되는 객체(예: state의 일부)를 포함하는 경우 해당 객체를 변이해서는 안된다.
    • ref.current 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않는다. ref는 일반 JavaScript 객체이기 때문에 React는 사용자가 언제 변경했는지 알지 못한다.
    • 초기화를 제외하고는 렌더링 중에 ref.current를 쓰거나 읽지 마라. 이렇게 하면 컴포넌트의 동작을 예측할 수 없게 된다.
    • Strict Mode에서 React는 컴포넌트 함수를 두 번 호출하여 의도하지 않은 변경을 찾을 수 있도록 돕는다. 이는 개발 환경 전용 동작이며 Production 환경에는 영향을 미치지 않는다. 각 ref 객체는 두 번 생성되고 그중 하나는 버려진다. 컴포넌트 함수가 순수하다면(그래야만 한다), 컴포넌트의 로직에 영향을 미치지 않는다.

    useRef 사용법

    ref로 값 참조하기

    컴포넌트의 최상위 레벨에서 useRef를 호출하여 하나 이상의 ref를 선언한다.

    import { useRef } from 'react';
     
    function Stopwatch() {
      const intervalRef = useRef(0);
      // ...

    useRef는 처음에 제공한 초기값으로 설정된 단일 current 프로퍼티가 있는 ref 객체를 반환한다.

    다음 렌더링에서 useRef는 동일한 객체를 반환한다. 정보를 저장하고 나중에 읽을 수 있도록 current 속성을 변경할 수 있다. state가 떠오를 수 있지만, 둘 사이에는 중요한 차이점이 있다.

    ref를 변경해도 리렌더링을 촉발하지 않는다. 즉 ref는 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는 데 적합하다.
    예를 들어 interval ID를 저장했다가 나중에 불러와야 하는 경우 ref에 넣을 수 있다. ref 내부의 값을 업데이트하려면 current 프로퍼티를 수동으로 변경해야 한다.

    function handleStartClick() {
      const intervalId = setInterval(() => {
        // ...
      }, 1000);
      intervalRef.current = intervalId;
    }

    나중에 ref에서 해당 interval ID를 읽어 해당 interval을 취소할 수 있다.

    function handleStopClick() {
      const intervalId = intervalRef.current;
      clearInterval(intervalId);
    }

    ref를 사용하면 다음을 보장한다.

    • (렌더링할 때마다 재설정되는 일반 변수와 달리) 리렌더링 사이에 정보를 저장할 수 있습니다.
    • (리렌더링을 촉발하는 state 변수와 달리) 변경해도 리렌더링을 촉발하지 않습니다.
    • (정보가 공유되는 외부 변수와 달리) 각각의 컴포넌트에 로컬로 저장됩니다.

    ref를 변경해도 다시 렌더링되지 않으므로 화면에 표시되는 정보를 저장하는 데는 ref가 적합하지 않다.
    만약 화면에 표시되는 정보를 저장하고 싶다면 state를 사용해야 한다.

    ref와 state의 차이

    useRef 예제 - Stopwatch

    아래 예시에서는 state와 ref의 조합을 사용한다 startTime과 now는 모두 렌더링에 사용되기 때문에 state 변수이다. 그러나 버튼을 누를 때 interval을 멈출 수 있게 하기 위해선 interval ID도 보유해야 한다. interval ID는 렌더링에 사용되지 않으므로 ref에 보관하고 수동으로 업데이트하는 것이 적절하다.

    import { useState, useRef } from 'react';
     
    export default function Stopwatch() {
      const [startTime, setStartTime] = useState(null);
      const [now, setNow] = useState(null);
      const intervalRef = useRef(null);
     
      function handleStart() {
        setStartTime(Date.now());
        setNow(Date.now());
     
        clearInterval(intervalRef.current);
        intervalRef.current = setInterval(() => {
          setNow(Date.now());
        }, 10);
      }
     
      function handleStop() {
        clearInterval(intervalRef.current);
      }
     
      let secondsPassed = 0;
      if (startTime != null && now != null) {
        secondsPassed = (now - startTime) / 1000;
      }
     
      return (
        <>
          <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
          <button onClick={handleStart}>Start</button>
          <button onClick={handleStop}>Stop</button>
        </>
      );
    }

    주의 사항

    렌더링 중에는 ref.current를 쓰거나 읽지 말자
    React는 컴포넌트의 본문이 순수 함수처럼 동작하기를 기대한다.

    • 입력값들(props, state, context)이 동일하면 완전히 동일한 JSX를 반환해야 합니다.
    • 다른 순서나 다른 인수를 사용하여 호출해도 다른 호출의 결과에 영향을 미치지 않아야 합니다.
      렌더링 중에 ref를 읽거나 쓰면 이러한 기대가 깨집니다.
    function MyComponent() {
      // ...
      // 🚩 Don't write a ref during rendering
      myRef.current = 123;
      // ...
      // 🚩 Don't read a ref during rendering
      return <h1>{myOtherRef.current}</h1>;
    }

    대신 이벤트 핸들러나 Effect에서 ref를 읽거나 쓸 수 있다.

    function MyComponent() {
      // ...
      useEffect(() => {
        // ✅ You can read or write refs in effects
        myRef.current = 123;
      });
      // ...
      function handleClick() {
        // ✅ You can read or write refs in event handlers
        doSomething(myOtherRef.current);
      }
      // ...
    }

    렌더링 중에 무언가를 읽거나 써야만 하는 경우, 대신 state를 사용하자.
    컴포넌트는 이러한 규칙을 어기더라도 여전히 작동할 수도 있지만, React에 추가되는 대부분의 새로운 기능들은 이러한 기대에 의존한다.

    ref로 DOM 조작하기

    ref를 사용하여 DOM을 조작하는 것은 특히 일반적이다. React에는 이를 위한 기본 지원이 있다.

    먼저 초기값이 null인 ref 객체를 선언하자.

    import { useRef } from 'react';
     
    function MyComponent() {
      const inputRef = useRef(null);
      // ...

    그런 다음 ref 객체를 ref 속성으로 조작하려는 DOM 노드의 JSX에 전달하자.

    // ...
    return <input ref={inputRef} />;

    React가 DOM 노드를 생성하고 화면에 그린 후, React는 ref 객체의 current프로퍼티를 DOM 노드로 설정하자. 이제 DOM 노드 <input> 접근해 focus()와 같은 메서드를 호출할 수 있다.

    function handleClick() {
      inputRef.current.focus();
    }

    ref 콘텐츠 재생성 피하기

    React는 초기에 ref 값을 한 번 저장하고, 다음 렌더링부터는 이를 무시한다.

    function Video() {
      const playerRef = useRef(new VideoPlayer());
      // ...

    new VideoPlayer()의 결과는 초기 렌더링에만 사용되지만, 호출 자체는 이후의 모든 렌더링에서도 여전히 계속 이뤄진다. 이는 값비싼 객체를 생성하는 경우 낭비일 수 있다.

    이 문제를 해결하려면 대신 다음과 같이 ref를 초기화할 수 있다.

    function Video() {
      const playerRef = useRef(null);
      if (playerRef.current === null) {
        playerRef.current = new VideoPlayer();
      }
      // ...

    일반적으로 렌더링 중에 ref.current를 쓰거나 읽는 것은 허용되지 않는다. 하지만 이 경우에는 결과가 항상 동일하고 초기화 중에만 조건이 실행되므로 충분히 예측할 수 있으므로 괜찮다.

    커스텀 컴포넌트에 대한 ref를 얻을 수 없는 문제 해결 방법

    컴포넌트에 ref를 전달하고자 다음과 같이 하면:

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

    다음과 같은 오류가 발생할 것이다.

    TypeError: Cannot read properties of null

    기본적으로 컴포넌트는 내부의 DOM 노드에 대한 ref를 외부로 노출하지 않는다.
    이 문제를 해결하려면 ref를 가져오고자 하는 컴포넌트를 찾아야 한다.

    export default function MyInput({ value, onChange }) {
      return <input value={value} onChange={onChange} />;
    }

    그리고 ref를 컴포넌트가 받는 props 목록에 추가한 뒤, 아래처럼 해당 자식 내장 컴포넌트에 prop으로 ref를 전달하자.

    function MyInput({ value, onChange, ref }) {
      return <input value={value} onChange={onChange} ref={ref} />;
    }
     
    export default MyInput;

    그러면 부모 컴포넌트가 ref를 가져올 수 있다.

    useImperativeHandle

    useImperativeHandle은 ref로 노출되는 핸들을 사용자가 직접 정의할 수 있게 해주는 React 훅이다.

    useImperativeHandle(ref, createHandle, dependencies?)

    컴포넌트의 최상위 레벨에서 useImperativeHandle 을 호출하여 노출할 ref 핸들을 사용자가 직접 정의할 수 있다.

    import { useImperativeHandle } from 'react';
     
    function MyInput({ ref }) {
      useImperativeHandle(ref, () => {
        return {
          // ... 메서드를 여기에 입력하세요 ...
        };
      }, []);
      // ...

    매개변수

    ref: MyInput 컴포넌트의 prop으로 받은 ref이다.

    createHandle: 인수가 없고 노출하려는 ref 핸들을 반환하는 함수이다. 해당 ref 핸들은 어떠한 유형이든 될 수 있다. 일반적으로 노출하려는 메서드가 있는 객체를 반환한다.

    (선택적) dependencies: createHandle 코드 내에서 참조하는 모든 반응형 값을 나열한 목록이다. 반응형 값은 props, state 및 컴포넌트 내에서 직접 선언한 모든 변수와 함수를 포함한다. React에 대한 린터를 구성한 경우 모든 반응형 값이 올바르게 의존성으로 지정되었는지 확인한다. 의존성 목록은 항상 일정한 수의 항목을 가지고 [dep1, dep2, dep3]와 같이 인라인으로 작성되어야 한다. React는 각 의존성을 Object.is 비교를 사용하여 이전 값과 비교한다. 리렌더링으로 인해 일부 의존성이 변경되거나 이 인수를 생략한 경우 createHandle함수가 다시 실행되고 새로 생성된 핸들이 ref에 할당된다.

    React 19 업데이트 내용

    React 19 부터 ref를 prop으로 받을 수 있다. React 18 또는 그 이전 버전에서는 ref를 받기위해 forwardRef를 사용해야 한다.
    Starting with React 19, ref is available as a prop. In React 18 and earlier, it was necessary to get the ref from forwardRef.

    반환값

    useImperativeHandle은 undefined를 반환한다.

    useImperativeHandle 사용법

    부모 컴포넌트에 커스텀 ref핸들 노출

    기본적으로 컴포넌트는 자식 컴포넌트의 DOM 노드를 부모 컴포넌트에 노출하지 않는다. 예를 들어 MyInput의 부모 컴포넌트가 <input> DOM 노드에 접근하려면 forwardRef를 사용하여 선택적으로 참조에 포함해야 한다.

    To expose a DOM node to the parent element, pass in the ref prop to the node.

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

    위의 코드에서 MyInput에 대한 ref는 <input> DOM 노드를 받게 된다. 그러나 대신 사용자 지정 값을 노출할 수 있다. 노출된 핸들을 사용자 정의하려면 컴포넌트의 최상위 레벨에서 useImperativeHandle을 호출하자.

    import { useImperativeHandle } from 'react';
     
    function MyInput({ ref }) {
      useImperativeHandle(ref, () => {
        return {
          // ... 메서드를 여기에 입력하세요 ...
        };
      }, []);
     
      return <input />;
    }

    위의 코드에서 <input>에 대한 ref는 더이상 전달되지 않는다.

    예를 들어 전체 <input> DOM 노드를 노출하지 않고 focus와 scrollIntoView의 두 메서드만을 노출하고 싶다고 가정해 보자. 그러기 위해서는 실제 브라우저 DOM을 별도의 ref에 유지해야 한다. 그리고 useImperativeHandle을 사용하여 부모 컴포넌트에서 호출할 메서드만 있는 핸들을 노출한다.

    import { useRef, useImperativeHandle } from 'react';
     
    function MyInput({ ref }) {
      const inputRef = useRef(null);
     
      useImperativeHandle(ref, () => {
        return {
          focus() {
            inputRef.current.focus();
          },
          scrollIntoView() {
            inputRef.current.scrollIntoView();
          },
        };
      }, []);
     
      return <input ref={inputRef} />;
    }

    이제 부모 컴포넌트가 MyInput에 대한 ref를 가져오면 focus 및 scrollIntoView 메서드를 호출할 수 있다. 그러나 기본 <input> DOM 노드의 전체 엑세스 권한은 없다.

    import { useRef } from 'react';
    import MyInput from './MyInput.js';
     
    export default function Form() {
      const ref = useRef(null);
     
      function handleClick() {
        ref.current.focus();
        // 이 작업은 DOM 노드가 노출되지 않으므로 작동하지 않습니다.
        // ref.current.style.opacity = 0.5;
      }
     
      return (
        <form>
          <MyInput placeholder='Enter your name' ref={ref} />
          <button type='button' onClick={handleClick}>
            Edit
          </button>
        </form>
      );
    }

    사용자 정의 imperative 메서드 노출

    imperative handle을 통해 노출하는 메서드는 DOM 메서드와 정확하게 일치할 필요가 없다. 예를 들어, 이 Post 컴포넌트는 imperative handle을 통해 scrollAndFocusAddComment 메서드를 표시한다. 이렇게 하면 부모 Page에서 버튼을 클릭할 때 댓글 목록을 스크롤하고 입력 필드에 초점을 맞출 수 있다.

    import { useRef } from 'react';
    import Post from './Post.js';
     
    export default function Page() {
      const postRef = useRef(null);
     
      function handleClick() {
        postRef.current.scrollAndFocusAddComment();
      }
     
      return (
        <>
          <button onClick={handleClick}>Write a comment</button>
          <Post ref={postRef} />
        </>
      );
    }

    주의 사항

    ref를 과도하게 사용하지 말자. ref는 props로 표현할 수 없는 필수적인 행동에만 사용해야 한다. 예를 들어 특정 노드로 스크롤 하기, 노드에 초점 맞추기, 애니메이션 촉발하기, 텍스트 선택하기 등이 있다.
    prop으로 표현할 수 있는 것은 ref를 사용하지 말자. 예를 들어 Modal 컴포넌트에서 { open, close } 와 같은 imperative handle을 노출하는 대신 <Modal isOpen={isOpen} />과 같은 isOpen prop을 사용하는 것이 더 좋다. Effects를 사용하면 prop을 통해 명령형 동작(imperative behavior)을 노출할 수 있다.

    Posted inreact
    Written byEunwoo