sohyeon kim

[React] 리액트 공식문서 정리 4(1) : 탈출구 - useRef, DOM 직접 조작, 리렌더링 방지 본문

React

[React] 리액트 공식문서 정리 4(1) : 탈출구 - useRef, DOM 직접 조작, 리렌더링 방지

aotoyae 2024. 11. 25. 23:06
728x90

 

 

💡 리액트 공식문서 정리 : 탈출구 - Ref 로 값 참조하기, Ref 로 DOM 조작하기

일부 컴포넌트는 React 외부의 시스템을 제어하고 동기화해야 할 수 있다.

e.g. 브라우저 API 를 사용해 input 에 초점 맞추기, React 없이 구현된 비디오 플레이어 재생, 원격 서버에 연결해 메시지 수신

이 장에선 React 의 '외부'로 나가 외부 시스템에 연결할 수 있는 탈출구를 배운다.

* 대부분의 애플리케이션 로직과 데이터 흐름은 이러한 기능에 의존해선 안된다.

 

1. Ref 로 값 참조하기 : 다시 렌더링하지 않고 정보를 '기억'하는 방법

컴포넌트 내 데이터를 유지하고 싶지만, 렌더링은 일으키고 싶지 않다면 ref 를 사용하자.

state 처럼 ref 로 값을 유지할 수 있다. 다만 state 는 리렌더링을 일으키지만, ref 는 그렇지 않다.

 

ref

  • React 가 추적하지 않는 컴포넌트의 비밀 주머니
  • React 의 단방향 데이터 흐름에서 '탈출구'가 되는 것!
  • ref.current 프로퍼티를 통해 해당 ref 의 현재 값에 접근 가능하다. 이 값은 읽거나 쓰거나 의도적인 변경이 가능
  • 렌더링에 영향을 주지 않는 timeout ID, DOM 엘리먼트 및 기타 객체를 저장할 수 있다.
  • ref 는 state 처럼 숫자, 문자열, 객체, 함수 등 모든 것을 가리킬 수 있다.
  • ref 는 state 와 달리 읽고 수정할 수 있는 current 프로퍼티를 가진 일반 자바스크립트 객체이다.
import { useRef } from 'react';

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

  function handleClick() {
    ref.current = ref.current + 1; // 클릭 시 ref.current 가 증가, 하지만 리렌더링되지 않는다.
    alert('You clicked ' + ref.current + ' times!');
  }

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

 

컴포넌트가 리렌더링 되면 모든 로컬 변수(state) 는 초기화된다. 대신 ref 에 저장하면 이 ref 는 렌더 사이에 React 에 보존된다.

🪄 어딘가에(비밀 주머니) 보관한 것

import { useState, useRef } from 'react';

export default function Chat() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const timeoutRef = useRef(null);

  function handleSend() {
    setIsSending(true);
    timeoutRef.current = setTimeout(() => { // timeout ID 를 저장.
      alert('Sent!');
      setIsSending(false);
    }, 3000);
  }

  function handleUndo() { // send 중 취소하는 핸들러
    setIsSending(false);
    clearTimeout(timeoutRef.current);
  }

  return (
    <>
      <input
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button
        disabled={isSending}
        onClick={handleSend}>
        {isSending ? 'Sending...' : 'Send'}
      </button>
      {isSending &&
        <button onClick={handleUndo}>
          Undo
        </button>
      }
    </>
  );
}
ref state
useRef(initialValue)  { current: initialValue } 을 반환 useState(initialValue) 은 state 변수의 현재 값과 setter 함수 [value, setValue] 를 반환
state를 바꿔도 리렌더 되지 않음 state를 바꾸면 리렌더
Mutable - 렌더링 프로세스 외부에서 current 값 수정 및 업데이트 가능. 리렌더링과 독립적으로 업데이트(최신 값을 유지) Immutable - state 를 수정하기 위해선 state 설정 함수를 반드시 사용하여 리렌더 대기열에 넣어야 함, 렌더링이 끝날 때까지 이전 값 사용
렌더링 중에 current 값을 읽거나 쓰면 안 된다. 렌더링 중 일부 정보가 필요하다면 state 를 대신 사용. ref.current 가 언제 변하는지 React 는 모르기 때문에 렌더링 시 읽어도 컴포넌트의 동작을 예측하기 어렵다. 언제든지 state 를 읽을 수 있다. 그러나 각 렌더마다 변경되지 않는 자체적인 state의 snapshot 이 있다.

 

2. Ref 로 DOM 조작하기 : React 가 관리하는 DOM 엘리먼트에 접근하는 방법

React 는 렌더링 결과물에 맞춰 DOM 변경을 자동으로 처리하기 때문에 자주 DOM 조작을 할 필요는 없다.

하지만 가끔 DOM 요소에 직접 접근해야 할 때가 있다. 이럴 때 ref 가 필요하다.

e.g. 특정 노드에 포커스, 스크롤 위치 이동, 위치와 크기 측정 시

<div ref={myRef}> <!-- 이제 React 는 이 DOM 노드를 myRef.current 에 넣는다. -->

<!-- 이 DOM 노드를 이벤트 핸들러에서 접근하거나 -->
<!-- 노드에 정의된 내장 브라우저 API 를 사용할 수 있다. -->
myRef.current.scrollIntoView();

 

스크롤 이동

한 컴포넌트에 하나 이상의 ref 를 가질 수 있다. 아래는 이미지 3개가 있는 캐러셀 예시이다.

import { useRef } from 'react';

export default function CatFriends() {
  const firstCatRef = useRef(null);
  const secondCatRef = useRef(null);
  const thirdCatRef = useRef(null);

// 각 버튼은 브라우저 scrollIntoView 메서드를 해당 DOM 노드로 호출해 이미지를 중앙에 배치한다.
  function handleScrollToFirstCat() {
    firstCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

// ...

  return (
    <>
      <nav>
        <button onClick={handleScrollToFirstCat}>
          Neo
        </button>
        // ...
      </nav>
      <div>
        <ul>
          <li>
            <img
              src="https://placecats.com/neo/300/200"
              alt="Neo"
              ref={firstCatRef}
            />
          </li>
          // ...
        </ul>
      </div>
    </>
  );
}

 

기본적으로 ref 로 다른 컴포넌트의 DOM 노드에 접근할 수 없다.

다른 컴포넌트의 DOM 노드를 조작하는 것은 오류를 일으킬 수 있다.

대신 DOM 노드를 선택적으로 노출해 ref 를 '전달'하도록 지정할 수 있다.

// SearchInput.js
import { forwardRef } from 'react';

export default forwardRef(
  function SearchInput(props, ref) { // 두 번째 인수로 상위의 inputRef 를 받는다.
    return (
      <input
        ref={ref}
        placeholder="Looking for something?"
      />
    );
  }
);
// App
import { useRef } from 'react';
import SearchInput from './SearchInput.js';

export default function Page() {
  const inputRef = useRef(null);
  
  return (
    <>
      <nav>
        <button onClick={() => {
          inputRef.current.focus();
        }}>
        Search
        </button
      </nav>
      <SearchInput ref={inputRef} />
    </>
  );
}

 

이 패턴은 디자인 시스템에서 버튼, 입력 요소 등 저수준 컴포넌트에서 DOM 노드를 전달하기 위해 흔하게 사용된다.

반면 폼, 목록, 페이지 섹션 등 고수준 컴포넌트에선 의도치 않은 DOM 구조 의존성 문제를 피하고자 일반적으로 DOM 노드를 노출하지 않는다.

 

React 가 ref 를 부여할 때

React 의 모든 갱신은 두 단계로 나눌 수 있다.

  • 렌더링 단계 : 화면에 무엇을 그려야 하는지 알아내도록 컴포넌트를 호출
  • 커밋 단계 : 변경사항을 DOM 에 적용

첫 렌더링에서 DOM 노드는 아직 생성되지 않아 ref.current 는 아직 null 인 상태이다.

갱신에 의한 렌더링에서 DOM 노드는 아직 업데이트 되지 않은 상태로, 두 상황 모두 ref 를 읽기에 너무 이른 상황이다.

 

React 는 ref.current 를 커밋 단계에서 설정한다. DOM 을 변경하기 전 React 는 관련된 ref.current 값을 미리 null 로 설정하고,

DOM 을 변경한 후 즉시 대응하는 DOM 노드로 다시 설정한다.

대부분 ref 접근은 이벤트 핸들러 내에서 일어난다. ref 를 사용해 뭔갈 하고 싶지만, 실행할 특정 이벤트가 없을 때 Effect 가 필요할 수도 있다.

 

➕ flushSync : 상태 업데이트를 동기적으로 처리해 강제로 즉시 렌더링을 실행

React 의 기본 상태 업데이트는 비동기적으로 처리되며, 최적화를 위해 배치 batch 로 묶어서 처리된다. (count 생각)

하지만 특정 상황에서 동기적으로 상태를 업데이트하고 즉시 렌더링 결과를 얻어야할 때가 있다. 이 때 flushSync 를 사용한다.

  • DOM 을 동기적으로 조작해야 할 때(DOM 조작과의 동기화가 필요할 때) 사용
  • 즉각적인 UI 업데이트(상태 업데이트)가 반영되어야 할 때 사용
  • React 의 기본 비동개 배치 처리 방식을 무시하므로 성능 최적화에 부정적인 영향을 줄 수 있으므로 꼭 필요한 경우에 사용
  • 비동기 함수 내에서 사용 불가

 

🔗 ref 와의 활용

import { flushSync } from 'react-dom';

function Counter() {
    const [count, setCount] = useState(0);

    function handleClick() {
        flushSync(() => { // 상태 업데이트를 동기적으로 처리
            setCount(count + 1);
        });

        console.log('Updated count:', count); // 동기적으로 업데이트된 count 값을 즉시 확인 가능
    }

    return (
        <>
            <p>Count: {count}</p>
            <button onClick={handleClick}>Increment</button>
        </>
    );
}

 

❗️DOM 을 직접 수정할 땐 다른 변경 사항과의 충돌을 주의해야 한다. React 가 관리하는 DOM 노드를 직접 바꾸려 하지 말 것.

상태에 따라 문구를 토글할 수 있는데, remove() 를 사용해 노드를 강제로 삭제하면 이후 토글은 에러가 발생한다.

다만 React 가 관리하지 않는(업데이트하지 않는) 빈 노드 같은 경우 추가하거나 삭제할 수 있다.

import {useState, useRef} from 'react';

export default function Counter() {
  const [show, setShow] = useState(true);
  const ref = useRef(null);

  return (
    <div>
      <button
        onClick={() => {
          setShow(!show);
        }}>
        Toggle with setState
      </button>
      <button
        onClick={() => {
          ref.current.remove();
        }}>
        Remove from the DOM
      </button>
      {show && <p ref={ref}>Hello world</p>}
    </div>
  );
}

 

 

 

🔗 https://ko.react.dev/learn/escape-hatches

 

탈출구 – React

The library for web and native user interfaces

ko.react.dev

 

 

 

728x90
반응형