본문 바로가기

FrontEnd

[3분 리액트] ref prop의 콜백 타입을 사용해 useEffect 사용 대체하기

반응형

원문 : https://tkdodo.eu/blog/avoiding-use-effect-with-callback-refs

 

Avoiding useEffect with callback refs

Interacting with DOM nodes doesn't necessarily need useEffect

tkdodo.eu

8월 14일에 올라온 글인데 누가 velog에 번역해서 올려뒀네요... 한국사람들 진짜 공부 열심히 하는것 같습니다.

TLDR

DOM 노드가 렌더링된 후 직접 상호 작용해야 하는 경우, useRef + useEffect 대신 ref 프롭의 콜백 형태 사용을 고려해보세요.

type Ref<T> = RefCallback<T> | RefObject<T> | null
  • 모든 ref 프롭은 함수다.(onAfterRender로 생각 가능하다.)
    • 리액트는 모든 렌더링 시 해당 함수를 실행한다.
      • 각 렌더링이 끝난 후 참조를 인자로 전달하여 해당 함수를 호출한다.
      • 언마운트시 null을 인자로 전달해 한번 더 실행한다.
    • ref에 동일 참조를 전달하면 컴포넌트가 렌더링 된 후 단 한번만 호출된다.
      • 렌더링 시 > 마운트 시로 변경되어, useEffect와 비슷해진다.
      • 컴포넌트의 라이프사이클과 밀접하게 동작한다
      • 코드를 줄일 수 있다. (useRef + useEffect > useCallback)

보통 useRef는 아래와 같이 씁니다.

function App() {
  const ref = React.useRef(null)

  React.useEffect(() => {
    // 🚨 ref.current is always null when this runs
    ref.current?.focus()
  }, [])

  return <Form ref={ref} />
}

const Form = React.forwardRef((props, ref) => {
  const [show, setShow] = React.useState(false)

  return (
    <form>
      <button type="button" onClick={() => setShow(true)}>
        show
      </button>
      // 🧐 ref is attached to the input, but it's conditionally rendered
      // so it won't be filled when the above effect runs
      {show && <input ref={ref} />}
    </form>
  )
})

의도는 마운트 시 포커스 입니다.

굉장히 뻔한 패턴인데, 의존성 배열에 show가 없어서 위 경우는 포커싱이 트리거되지 않습니다.

다음과 같은 일이 발생합니다.
  • Form이 렌더링됩니다.
  • input이 렌더링되지 않고 ref는 여전히 null입니다.
  • effect가 실행되고 아무 작업도 수행하지 않습니다.
  • input이 렌더링되면 ref가 채워지지만 효과가 다시 실행되지 않기 때문에 초점이 맞춰지지 않습니다.
문제는 effect 훅이 Form의 렌더링 함수에 갖혀 있지만,
실제로는 "폼이 마운트될 때"가 아니라 "input이 렌더링될 때 input에 포커스"를 표현하고자 한다는 것입니다.

Callback refs

ref에 대한 타입 선언(type declarations for refs)을 본 적이 있다면,

ref 객체뿐만 아니라 함수도 전달할 수 있음을 알 수 있습니다.

type Ref<T> = RefCallback<T> | RefObject<T> | null
개념적으로 저는 React 엘리먼트에 대한 참조를 컴포넌트가 렌더링된 후 호출되는 함수로 생각하는 것을 좋아합니다.
  • 이 함수는 인수로 전달된 렌더링된 DOM 노드를 가져옵니다.
  • React 요소가 마운트 해제되면 null로 다시 한 번 호출됩니다.
따라서 React 엘리먼트의 ref 프롭으로 useRef(RefObject-객체 형태)를 전달하는 것은 아래 코드의 문법적 설탕입니다.
(주 : 물론 다형성이라 생각할 수도 있음)
<input
  ref={(node) => {
    ref.current = node;
  }}
  defaultValue="Hello world"
/>

다시 한 번 강조하자면

모든 ref props는 함수일 뿐입니다.

이 함수는 렌더링 후에 실행되며, 여기서 부작용을 실행하는 것은 전혀 문제가 되지 않습니다.
ref 프롭 이름이 그냥 onAfterRender나 비슷한 이름이었으면 더 좋았을 것 같습니다.

이제 이걸 알았으니 콜백 ref안에서 인풋에 포커싱하지 않을 이유가 있을까요?

(폼에서 > input으로 함수 이동)

<input
  ref={(node) => {
    node?.focus()
  }}
  defaultValue="Hello world"
/>

사실 알아두어야 할게 있는데요

React는 모든 렌더링 후에 이 함수를 실행합니다!

따라서 입력에 자주 포커싱하는 것이 좋지 않다면(그렇지 않을 가능성이 높습니다)
우리가 원할 때만 이것을 실행하도록 React에 지시해야 합니다.

useCallback과 함께 사용하기

React는 참조 안정성을 사용하여 callback ref를 실행해야 하는지 여부를 확인합니다.

즉, ref에 동일한 객체를 전달하면 실행을 건너뜁니다.
따라서 useCallback을 사용할 수 있습니다.
const ref = React.useCallback((node) => {
  node?.focus()
}, [])

return <input ref={ref} defaultValue="Hello world" />

 

이것을 초기 버전과 비교하면
  • 코드가 적고 두 개(useRef + useEffect) 대신 한 개의 훅(useCallback)만 사용합니다.
  • callback ref는 이를 마운트하는 컴포넌트가 아니라 DOM 노드의 수명 주기에 바인딩되기 때문에 모든 상황에서 작동합니다.
  • 엄격 모드(개발 환경에서 실행할 때)에서는 두 번 실행되지 않습니다. 
(이전) React 문서의 이 숨겨진 보석(hidden gem in the (old) React docs)에서 볼 수 있듯이,
이를 사용하여 모든 종류의 부작용을 실행할 수 있습니다.
예를 들어 안에서 setState를 호출하는 것도 가능합니다. 실제로 꽤 좋습니다.
function MeasureExample() {
  const [height, setHeight] = React.useState(0)

  const measuredRef = React.useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height)
    }
  }, [])

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  )
}
앞으로 DOM 노드가 렌더링된 후 직접 상호 작용해야 하는 경우
useRef + useEffect 대신 ref 프롭의 콜백 형태 사용을 고려해보세요.

 

반응형