useRef

김동현·2026년 3월 17일

useRef

소개

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

const ref = useRef(initialValue)

목차


레퍼런스 {#reference}

useRef(initialValue) {#useref}

컴포넌트의 최상위 레벨에서 useRef를 호출해서 ref를 선언하세요.

import { useRef } from 'react';

function MyComponent() {
  const intervalRef = useRef(0);
  const inputRef = useRef(null);
  // ...

아래에서 더 많은 예제를 확인하세요.

매개변수 {#parameters}

  • initialValue: ref 객체의 current 프로퍼티가 처음에 가질 값이에요. 어떤 타입의 값이든 넣을 수 있어요. 이 인자는 초기 렌더링 이후에는 무시돼요.

반환값 {#returns}

useRef는 하나의 프로퍼티를 가진 객체를 반환해요:

  • current: 처음에는 여러분이 전달한 initialValue로 설정돼요. 나중에 다른 값으로 바꿀 수 있어요. ref 객체를 JSX 노드의 ref 속성으로 React에 전달하면, React가 current 프로퍼티를 해당 DOM 노드로 설정해줘요.

다음 렌더링에서도 useRef는 동일한 객체를 반환해요.

주의사항 {#caveats}

  • ref.current 프로퍼티는 변경할 수 있어요(mutate 가능). state와 달리 mutable(변경 가능)하거든요. 하지만 렌더링에 사용되는 객체(예를 들어 state의 일부)를 담고 있다면, 그 객체를 직접 변경하면 안 돼요.
  • ref.current 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않아요. ref는 그냥 평범한 JavaScript 객체이기 때문에, 여러분이 그걸 변경해도 React는 알 수가 없어요.
  • 초기화를 제외하고는, 렌더링 중에 ref.current를 쓰거나 읽지 마세요. 이렇게 하면 컴포넌트의 동작을 예측할 수 없게 돼요.
  • Strict Mode에서는 React가 의도치 않은 불순함(impurity)을 찾아내기 위해 컴포넌트 함수를 두 번 호출해요. 이건 개발 모드에서만 일어나는 동작이고 프로덕션에는 영향을 미치지 않아요. 각 ref 객체가 두 번 생성되지만 하나는 버려져요. 컴포넌트 함수가 순수하다면(그래야 하고요), 이것이 동작에 영향을 미치지 않을 거예요.

사용법 {#usage}

ref로 값 참조하기 {#referencing-a-value-with-a-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를 대신 사용하세요. useRefuseState 중에 선택하는 방법에 대해 더 읽어보세요.

useRef로 값 참조하기 예제들

클릭 카운터 {#click-counter}

이 컴포넌트는 버튼이 몇 번 클릭되었는지 추적하기 위해 ref를 사용해요. 여기서 state 대신 ref를 사용하는 게 괜찮은 이유는 클릭 횟수가 이벤트 핸들러에서만 읽히고 쓰이기 때문이에요.

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

만약 JSX에서 {ref.current}를 보여주면, 클릭해도 숫자가 업데이트되지 않을 거예요. ref.current를 설정하는 것은 리렌더링을 트리거하지 않으니까요. 렌더링에 사용되는 정보는 state로 관리해야 해요.

스톱워치 {#a-stopwatch}

이 예제는 state와 ref의 조합을 사용해요. startTimenow는 둘 다 렌더링에 사용되기 때문에 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() {
  // ...
  // 🚩 렌더링 중에 ref를 쓰지 마세요
  myRef.current = 123;
  // ...
  // 🚩 렌더링 중에 ref를 읽지 마세요
  return <h1>{myOtherRef.current}</h1>;
}

대신 이벤트 핸들러나 effect에서 ref를 읽거나 쓸 수 있어요.

function MyComponent() {
  // ...
  useEffect(() => {
    // ✅ effect에서 ref를 읽거나 쓸 수 있어요
    myRef.current = 123;
  });
  // ...
  function handleClick() {
    // ✅ 이벤트 핸들러에서 ref를 읽거나 쓸 수 있어요
    doSomething(myOtherRef.current);
  }
  // ...
}

만약 렌더링 중에 무언가를 읽거나 써야 한다면, 대신 state를 사용하세요.

이 규칙을 어기더라도 컴포넌트가 여전히 작동할 수도 있지만, React에 추가하고 있는 대부분의 새로운 기능들은 이런 기대에 의존하고 있어요. 컴포넌트를 순수하게 유지하는 것에 대해 더 읽어보세요.


ref로 DOM 조작하기 {#manipulating-the-dom-with-a-ref}

ref를 사용해서 DOM을 조작하는 건 특히 흔한 패턴이에요. React는 이걸 기본적으로 지원하고 있어요.

먼저, 초기값null로 해서 ref 객체를 선언하세요:

import { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);
  // ...

그다음 여러분의 ref 객체를 조작하고 싶은 DOM 노드의 JSX에 ref 속성으로 전달하세요:

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

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

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

노드가 화면에서 제거되면 React는 current 프로퍼티를 다시 null로 설정해줘요.

ref로 DOM 조작하기에 대해 더 읽어보세요.

useRef로 DOM 조작하기 예제들

텍스트 input에 포커스 주기 {#focusing-a-text-input}

이 예제에서는 버튼을 클릭하면 input에 포커스가 가요:

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

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

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

이미지를 뷰로 스크롤하기 {#scrolling-an-image-into-view}

이 예제에서는 버튼을 클릭하면 이미지가 뷰로 스크롤돼요. 리스트 DOM 노드에 대한 ref를 사용하고, 그 다음 DOM querySelectorAll API를 호출해서 스크롤하고 싶은 이미지를 찾아요.

import { useRef } from 'react';

export default function CatFriends() {
  const listRef = useRef(null);

  function scrollToIndex(index) {
    const listNode = listRef.current;
    // This line assumes a particular DOM structure:
    const imgNode = listNode.querySelectorAll('li > img')[index];
    imgNode.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToIndex(0)}>
          Neo
        </button>
        <button onClick={() => scrollToIndex(1)}>
          Millie
        </button>
        <button onClick={() => scrollToIndex(2)}>
          Bella
        </button>
      </nav>
      <div>
        <ul ref={listRef}>
          <li>
            <img
              src="https://placecats.com/neo/300/200"
              alt="Neo"
            />
          </li>
          <li>
            <img
              src="https://placecats.com/millie/200/200"
              alt="Millie"
            />
          </li>
          <li>
            <img
              src="https://placecats.com/bella/199/200"
              alt="Bella"
            />
          </li>
        </ul>
      </div>
    </>
  );
}
div {
  width: 100%;
  overflow: hidden;
}

nav {
  text-align: center;
}

button {
  margin: .25rem;
}

ul,
li {
  list-style: none;
  white-space: nowrap;
}

li {
  display: inline;
  padding: 0.5rem;
}

비디오 재생 및 일시정지 {#playing-and-pausing-a-video}

이 예제는 <video> DOM 노드에서 play()pause()를 호출하기 위해 ref를 사용해요.

import { useState, useRef } from 'react';

export default function VideoPlayer() {
  const [isPlaying, setIsPlaying] = useState(false);
  const ref = useRef(null);

  function handleClick() {
    const nextIsPlaying = !isPlaying;
    setIsPlaying(nextIsPlaying);

    if (nextIsPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }

  return (
    <>
      <button onClick={handleClick}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <video
        width="250"
        ref={ref}
        onPlay={() => setIsPlaying(true)}
        onPause={() => setIsPlaying(false)}
      >
        <source
          src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
          type="video/mp4"
        />
      </video>
    </>
  );
}
button { display: block; margin-bottom: 20px; }

자신의 컴포넌트에 ref 노출하기 {#exposing-a-ref-to-your-own-component}

가끔은 부모 컴포넌트가 여러분의 컴포넌트 안의 DOM을 조작할 수 있게 하고 싶을 수 있어요. 예를 들어, MyInput 컴포넌트를 만들고 있는데 부모가 input에 포커스를 줄 수 있게 하고 싶다면요(부모는 해당 input에 직접 접근할 수 없으니까요). 부모에서 ref를 만들고 자식 컴포넌트에 ref를 prop으로 전달하면 돼요. 자세한 설명은 여기서 읽어보세요.

import { useRef } from 'react';

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

export default function Form() {
  const inputRef = useRef(null);

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

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

ref 내용의 재생성 피하기 {#avoiding-recreating-the-ref-contents}

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를 쓰거나 읽는 건 허용되지 않아요. 하지만 이 경우에는 괜찮아요. 왜냐하면 결과가 항상 같고, 조건문이 초기화 중에만 실행되기 때문에 완전히 예측 가능하거든요.

심화: useRef를 나중에 초기화할 때 null 체크를 피하는 방법 {#how-to-avoid-null-checks-when-initializing-use-ref-later}

타입 체커를 사용하는데 항상 null 체크를 하고 싶지 않다면, 이런 패턴을 시도해볼 수 있어요:

function Video() {
  const playerRef = useRef(null);

  function getPlayer() {
    if (playerRef.current !== null) {
      return playerRef.current;
    }
    const player = new VideoPlayer();
    playerRef.current = player;
    return player;
  }

  // ...

여기서 playerRef 자체는 nullable이에요. 하지만 getPlayer()null을 반환하는 경우는 없다는 걸 타입 체커에게 납득시킬 수 있을 거예요. 그러면 이벤트 핸들러에서 getPlayer()를 사용하면 돼요.


문제 해결 {#troubleshooting}

커스텀 컴포넌트에 ref를 전달할 수 없어요 {#i-cant-get-a-ref-to-a-custom-component}

여러분의 컴포넌트에 이렇게 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}
    />
  );
}

그리고 컴포넌트가 받는 props 목록에 ref를 추가하고, 관련 자식 빌트인 컴포넌트ref를 prop으로 전달하세요. 이렇게요:

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

export default MyInput;

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

다른 컴포넌트의 DOM 노드에 접근하기에 대해 더 읽어보세요.


사이트맵

모든 문서 페이지 개요

profile
프론트에_가까운_풀스택_개발자

0개의 댓글