React Ref Callback

jh·2024년 4월 8일

참고문서 - avoiding-use-effect-with-callback-refs

  • ForwardedRef 의 타입을 알아보던 중에 좋은 글을 발견해서 작성했습니다
  • ref를 사용하는 자세한 방법은 제외

ref란?

리액트 공식 문서에서는

When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref.

  • 어떠한 정보(데이터 값)을 기억(저장)하고 싶은데, 이 정보가 새로운 렌더링을 유발하고 싶지 않는 경우 ref를 사용한다

해당 ref에 저장되어 있는 값이 변해도, 이것이 재 렌더링을 유발하지는 않는다

이 성질을 이용하여 DOM을 직접적으로 조작 할때에 주로 사용된다

React automatically updates the DOM to match your render output, so your components won’t often need to manipulate it. However, sometimes you might need access to the DOM elements managed by React—for example, to focus a node, scroll to it, or measure its size and position. There is no built-in way to do those things in React, so you will need a ref to the DOM node.

  • 리액트에서 자동으로 DOM을 업데이트 해주기 때문에 직접 조작할 필요는 거의 없지만, 포커스, 스크롤, 사이즈 조절 같이 리액트에서 직접적으로 해주지 못하는 동작을 위해서 ref를 사용할 필요가 있다

Ref + useEffect

위에서 설명했던 포커스를 자동 설정하는 가장 일반적인 코드를 한번 짜보면

const ref = useRef(null)

useEffect(() => 
		ref.current?.focus(),[])

return <input ref={ref}/>
  1. input 컴포넌트가 렌더링되면서, ref.current에 값이 생긴다
  2. useEffect가 실행되면서 ref.current 값인 input에 focus가 생긴다

그렇다면 이 코드에서는 ?

const Form = React.forwardRef((props, ref) => {
  const [show, setShow] = React.useState(false)
  
  useEffect(() => 
		ref.current?.focus(),[])
  
  return (
    <form>
      <button type="button" onClick={() => setShow(true)}>
        show
      </button>
      {show && <input ref={ref} />}
    </form>
  )
})

제일 처음 렌더링 되었을 때, ref.current는 null 이다.
만약 버튼을 눌러도 useEffect의 의존성 배열 원소가 없기 때문에 useEffect 안의 콜백함수는 실행되지 않을테고, focus가 생기지 않는다

  • useEffect 의존성으로 show 를 넣어줄 수도 있겠지만, 만약에 ref를 외부에서 props로 전달받는다면?(ref forwarding)
export default function App() {
  const ref = useRef(null)

  useEffect(() => {
    // ref.current는 null
    ref.current?.focus()
  }, [])

  return <Form ref={ref} />
}

const Form = forwardRef((props, ref) => {
  const [show, setShow] = useState(false)
  
  return (
    <form>
      <button type="button" onClick={() => setShow(!show)}>
        show
      </button>
      {show && <input ref={ref} />}
    </form>
  )
})

Ref Callback

ForwardedRef<T> 라는 타입을 자세히 보면
((instance: T | null) => void) 라는 함수 타입이 포함되어 있는 걸 알 수 있다

이 말은 ref를

<input ref={ref}/>

<input ref={(node) => ref.current = node}/>

이런 식으로 선언할 수 있다는 뜻이다
메인테이너의 말을 빌리면

I like to think about refs on React elements as functions that are called after the component has rendered. This function gets the rendered DOM node passed as argument. If the React element unmounts, it will be called once more with null.

  • React Element의 ref는 컴포넌트가 렌더링된 이후 실행되는 함수라고 생각한다. 이 함수는 렌더링된 DOM node를 인자로 받고, 해당 컴포넌트가 언마운트되면 한번 더 호출되어 null을 인자로 받는다

  • 콘솔을 찍어보면 해당 컴포넌트가 마운트 될 경우 인자로 DOM node 가 찍히고, 언마운트된 후에는 null 이 찍힌다

그렇다면 굳이 useEffect 를 사용할 필요 없이

<input ref={(node) => node?.focus()}/>

로 바꿔주면 원하는대로 동작한다

useCallback 사용하기

위의 경우를 살펴보면, 사실 useRef를 통해 ref 객체(RefObject)를 만들 필요가 없다는 걸 알 수 있다

  • 그냥 node를 인자로 받아서 실행할 함수 만 있으면 된다

그렇다면 코드를

const ref = (node) => node?.focus()
  
  return <Form ref={ref} />

이런 식으로 변경할 수 있고,
해당 함수를 useCallback으로 감싸주면 렌더링 관련해서 약간의 최적화를 해줄 수도 있다

const ref = React.useCallback((node) => {
  node?.focus()
}, [])

return <Form ref={ref}/>

0개의 댓글