개발자의 공부/React

[React]useRef

2023. 1. 28. 11:49
목차
  1. ✅ useRef란?
  2. 🤔사용해 보기
  3. 👉 다른 컴포넌트의 DOM 노드에 접근하기

해당 내용은 2023년 01월 28일 React Docs BETA 버전을 토대로 작성되었습니다. - 링크

✅ useRef란?

렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook이다. 여기서 Ref는 reference, 즉 참조를 뜻한다. 값을 참조하고 DOM을 조작하는 데에 주로 사용된다.

참조란? 객체의 실제 위치를 가리키는 포인터이다. 실제 값이 아닌 메모리 공간의 주소를 가진다.
const ref = useRef(initialValue)

매개변수

  • initialValue: 참조하려는 ref 객체의 current property의 초기 값이다.

반환

단일 속성(current)을 가진 객체를 반환한다.

  • current: 처음에는 매개변수로 받은 초기 값으로 설정되어 있고, ref 객체를 JSX 노드 속성으로 React에 전달하면 해당 노드 current 속성을 설정한다. 간단하게 말하자면, <div>에 ref를 설정하면 <div>가 current 속성으로 설정된다는 것이다.

주의 사항

  • state와 달리 ref.current 속성을 변경할 수 있다. 하지만 렌더링에 사용되는 객체(예. 상태의 일부)를 보유하는 경우 해당 객체를 변경해서는 안 된다.
  • ref.current 속성을 변경할 때, 컴포넌트가 리렌더링 되지는 않는다. ref는 일반 JavaScript 객체이기 때문에 React는 언제 변하는지 모른다.
  • 초기화를 제외하고는 렌더링 중에 ref.current를 읽거나 쓰면 안 된다. 이로 인해 컴포넌트의 동작을 예측할 수 없게 된다.
  • Strict Mode에서, 컴포넌트가 두 번 실행되는데 이것은 각 ref 객체가 두 번 생성되고 하나는 삭제된다. 그러므로 컴포넌트가 순수한 경우, 컴포넌트 로직에 영향을 미치지 않는다.

🤔사용해 보기

🟢 1. ref로 값 참조하기

위에서 이야기했듯이 useRef는 처음에 제공된 초기값으로 설정된 current 속성 하나만 가진 ref 객체를 반환한다. 

 

그다음 렌더링에서, useRef는 같은 객체를 반환하며 current 속성을 변경하여 정보를 저장하고 나중에 읽을 수 있다. state와 비슷한 느낌이 들지만 중요한 차이는 ref를 변경해도 리렌더링이 발생하지 않는다. 즉, ref는 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는 데에 적합하다. 이 부분의 설명이 어렵다면 코드를 보면서 이해하자.

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>
  );
}

위 컴포넌트는 ref를 사용하여 버튼 클릭 횟수를 추적한다. 클릭 수는 이벤트 헨들러에서만 읽고 쓰기 때문에 여기에서는 상태 대신 ref를 사용해도 괜찮다. 

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>
    </>
  );
}

setInterval은 IntervalID를 반환하는데 해당 정보를 Ref에 담아주었다. 해당 정보를 가지고 동일한 함수의 동작을 중지할 수 있다. intervalRef.current가 IntervalID를 보유하고 있기에 clearInterval이 적절하게 실행될 수 있다.

  • 리렌더링 사이에 정보를 저장할 수 있다. (렌더링 될 때마다 재설정되는 일반 변수와 달리)
  • 변경해도 리렌더링 되지 않는다. (변경됨에 따라 리렌더링을 유발하는 상태 변수와 달리)
  • 정보는 컴포넌트의 각 복사본에 로컬이다. (공유되는 외부 변수와 달리)

🟡 함정

렌더링 중에 ref.current를 쓰거나 읽지 마라. 항상 말해왔듯이 React 컴포넌트는 순수해야 한다. 

  • 입력(props, state 및 context)이 동일한 경우 정확히 동일한 JSX를 반환해야 한다.
  • 다른 순서로 호출하거나 다른 인자로 호출해도 다른 호출 결과에 영향을 미쳐선 안된다.
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를 사용해야 한다.

🟢 2. ref로 DOM 조작하기

먼저 아래와 같이 ref 객체를 선언하고 조작하려는 DOM 노드의 JSX에 속성으로 ref 객체를 전달한다.

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

여기서 React는 DOM 노드를 생성하고 화면에 표시한 후 ref 객체의 current 속성을 해당 DOM 노드로 설정한다. 이렇게 하면 <input>의 DOM 노드에 접근할 수 있기에 focus()와 같은 메서드를 호출할 수 있다.

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

React는 해당 노드가 화면에서 제거될 대 null로 current 속성을 다시 설정한다.

예시 1. 이미지를 스크롤하여 보기

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)}>
          Tom
        </button>
        <button onClick={() => scrollToIndex(1)}>
          Maru
        </button>
        <button onClick={() => scrollToIndex(2)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul ref={listRef}>
          <li>
            <img
              src="https://placekitten.com/g/200/200"
              alt="Tom"
            />
          </li>
          <li>
          ...
          </li>
        </ul>
      </div>
    </>
  );
}

버튼을 클릭하면 해당 이미지로 스크롤된다. <li> DOM 노드에 대한 참조를 사용하고, DOM querySelectorAll API를 호출하여 스크롤하기를 원하는 이미지를 찾는다.

예시 2. 비디오 재생 및 일시 중지

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>
    </>
  );
}

video DOM 노드에서 play()와 pause() 호출

🟢 ref content 재생성 방지하기

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

new VideoPlayer()의 결과는 초기 렌더링에서만 사용되지만, 이 함수는 모든 렌더링에서 호출되고 있다. 이 문제를 해결해 보자. 

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

ref.current는 일반적으로 렌더링 중에 쓰거나 읽으면 안 되지만 이 경우에는 결과가 항상 같고, 해당 조건문은 초기화 중에만 실행되므로 완전히 예측할 수 있기에 괜찮다.

 

나중에 useRef를 초기화할 때 null check를 피하는 방법

type을 검사하고 항상 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 자체는 null을 허용하지만 getPlayer()는 null을 반환하지 않는다고 type checker에게 이야기하고 있다. 그런 다음 이벤트 헨들러에서 getPlayer()를 사용한다.

 

👉 다른 컴포넌트의 DOM 노드에 접근하기

저작자표시 비영리 동일조건 (새창열림)

'개발자의 공부 > React' 카테고리의 다른 글

이미지 최적화에 대한 명확한 가이드  (0) 2023.02.01
프론트엔드 최적화 시도 - 1  (0) 2023.01.30
[React]Page Routing(v6.4)  (0) 2022.11.28
[React]Google 로그인 OAuth2로 다른 Google API와 연동  (0) 2022.11.27
[React]useContext  (0) 2022.11.15
  1. ✅ useRef란?
  2. 🤔사용해 보기
  3. 👉 다른 컴포넌트의 DOM 노드에 접근하기
'개발자의 공부/React' 카테고리의 다른 글
  • 이미지 최적화에 대한 명확한 가이드
  • 프론트엔드 최적화 시도 - 1
  • [React]Page Routing(v6.4)
  • [React]Google 로그인 OAuth2로 다른 Google API와 연동
JMins
JMins
안녕하세요. 프론트엔드 개발자 김정민입니다. 개발 지식을 공유하고 기록하는 공간입니다.
JMins
개발자 노트
JMins
전체
오늘
어제
  • 분류 전체보기 (85)
    • 개발자의 공부 (73)
      • React (17)
      • 자료구조&알고리즘 (28)
      • JS (17)
      • TS (8)
      • Nodejs (0)
      • Nextjs (1)
      • 기타 (1)
      • Design Pattenrs (0)
      • 테스트 및 최적화 (1)
    • 문제 및 해결 (9)
    • 기본 지식 (3)
    • 챌린지 (0)

블로그 메뉴

  • 홈

공지사항

  • #블로그 스킨 변경
  • 개인적으로 공부를 기록하기 위해 적고 있습니다.

인기 글

최근 댓글

최근 글

hELLO · Designed By 정상우.
JMins
[React]useRef
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.