useRef의 정체를 파헤쳐보자

이은지·2022년 11월 25일
0
post-thumbnail

✅ TL;DR

  • useRef: 리액트에서 특정 DOM 요소에 접근하는 방법이다.
  • 리액트에서 제공하지 않는 브라우저 API를 사용해야 할 때 사용한다. (스크롤 위치 제어, 특정 요소에 포커싱하기 등)
  • 컴포넌트의 DOM node를 컴포넌트 외부에서 접근하는 것은 기본적으로 불가능하다. 리액트가 막아놨다. 외부 접근을 허용하려면 forwardRef API를 사용해 컴포넌트를 선언해야 한다.

순수 자바스크립트와 달리, 리액트를 사용하면 DOM에 직접 접근할 일이 없다. 기본적으로 리액트가 우리의 render 결과물에 맞게 DOM을 알아서 업데이트 해주기 때문이다. 그러나 DOM 요소에 직접 접근해야 하는 몇 가지 경우들이 존재한다.

🤔 DOM 요소에 접근해야 하는 경우

  • 스크롤 위치를 제어해야 할 때
  • 특정 요소에 포커스를 맞춰야 할 때

이처럼 리액트가 제공하지 않는 브라우저 API를 사용해야 할 때 우리는 DOM 요소에 직접 접근하게 된다.

🤜🏻 리액트에서 DOM 요소에 접근하는 방법: useRef

import { useRef } from 'react';

const myRef = useRef(null);

<div ref={myRef}/> 
// DOM node에 myRef를 ref 속성으로 전달한다. 이는 리액트에게 이 <div>의 DOM node를 myRef.current에 주입해달라고 말하는 것이다. 

useRef는 current 라는 프로퍼티를 가진 한 객체를 리턴한다.

이때 current 프로퍼티의 기본값은 null이다. 리액트가 <div> 태그에 상응하는 DOM node를 생성하면, 이 DOM node에 대한 레퍼런스가 current 에 주입된다.

// 바닐라 자바스크립트
const myDiv = document.querySelector('.my-div');
myDiv.addEventListener('click', callbackFn);

// 리액트
const myDiv = useRef(null);
<div ref={myDiv}/>
myDiv.current.addEventListener('click', callbackFn);

바닐라 자바스크립트와 비교하면 위와 같다. 바닐라 자바스크립트에서는 querySelector를 사용해 내가 원하는 DOM 요소에 접근했다. 리액트에서는 querySelector 대신 useRef를 사용해 원하는 DOM 요소에 접근할 수 있다! document.querySelector() 가 반환하는 값이 myRef.current 에 똑같이 담겨있다.

좀 더 정확히 표현하자면 리액트에 의해 관리되는 DOM 요소에 접근하는 것이다. 앞서 언급했듯 리액트에서는 DOM의 업데이트를 리액트가 관장한다. 따라서 그냥 DOM이 아닌 리액트가 관리하는 DOM에 접근해야 한다.

(참고로 리액트에서 window.document.querySelector() 를 사용해 DOM 요소에 접근하려고 하면 “window 객체가 정의되지 않았다”는 에러가 뜬다.)

myRef.current를 사용하면 DOM 요소에 내장된 브라우저 API를 호출할 수 있다. (scrollIntoView, focus 등)

🤜🏻 forwardRef

useRef를 이해 했다면, 다음의 코드를 살펴보자.

const MyInput = (ref) => {
	return <input ref={ref}>까꿍</input>
}

const MyForm = () => {
	const myRef = useRef(null);
	const handleClick = () => {
		myRef.current.focus();
	}
	return (
	<>
		<button onClick={handleClick}>클릭</button>
		<MyInput ref={myRef}/>
	</>
	)
}

이 코드가 어떻게 작동할지 예상해보자.


myRef.current에는 MyInput의 input 에 대한 레퍼런스가 들어있을테니까, 버튼을 클릭하면 해당 입력폼에 포커스가 맞춰지겠군!

라고 생각했다면 땡이다. 우리는 원하던 결과 대신 런타임 에러를 마주하게 된다.


"myRef.current 가 null이기 때문에 프로퍼티를 읽을 수 없다"는 에러가 뜬다.
(Cannot read properties of null)

즉, input 에 대한 DOM node(의 레퍼런스)가 myRef.current에 주입되지 않은 것이다. 왜그럴까?

리액트는 한 컴포넌트가 다른 컴포넌트의 DOM node에 접근하는 것을 허용하지 않기 때문이다. 리액트에서 DOM node에 직접 접근하는 일은 가끔씩만 발생해야 하는 예외에 해당하기 때문에, 리액트는 컴포넌트가 다른 컴포넌트의 DOM node에 접근하는 것을 막아놓았다.

따라서 만약 특정 컴포넌트의 DOM node에 대한 접근을 허용하고 싶다면, 리액트에게 이야기해줘야 한다. “내 DOM node는 외부에서 써도 돼!” 하고 말이다. forwardRef API를 사용하면 리액트에게 이 사실을 알려줄 수 있다.

const MyInput = forwardRef((ref) => {
  return <input {...props} ref={ref} />;
});

forwardRef 를 이용해 컴포넌트를 선언했다는 것은 해당 컴포넌트의 DOM node가 외부에서 접근 가능하다는 뜻이다. 이제 myRef.current 에 DOM node에 대한 레퍼런스가 정상적으로 주입된다.

❌ 주의사항

리액트로 해결되지 않는 경우에만 사용할 것
남용할 경우 리액트의 DOM 업데이트가 예상과 다르게 동작할 수 있다.

📓 References

Manipulating the DOM with Refs

profile
교육학과 출신 서타터업 프론트 개발자 👩🏻‍🏫

0개의 댓글