React useRef의 다양한 활용 방법(mutable object, callback ref와 forwardRef)
리액트에서 render() 메서드에 의해 만들어지는 DOM에 접근하는 방식 으로 ref 를 제공한다. 예를 들어 배송지 정보를 입력 받아야 하는 결제 페이지를 만들 때, 사용자가 휴대번호와 같은 필수 정보를 입력하지 않고 결제하기 버튼을 눌렀다면 ref 를 사용하여 휴대번호 input 창에 focus 할 수 있다. 또는 특정 element 의 크기를 가져오거나 이 요소가 DOM 으로부터 얼마만큼 떨어져 있는지 스크롤의 위치를 구할 수도 있다.
이 글은 앞서 설명한 상황에서 ref 를 사용해봤지만
- ref.current에 HTMLElement 만을 할당해보신 분
- ref.current의 값이 변경될 때 re-rendering이 발생하지 않는다는 것을 모르시는 분
- forwardRef, callback ref 에 대해 들어보지 못하신 분
을 대상으로 하는 useRef의 다양한 활용 방법(심화)에 관한 글이다.
우리가 무언가를 배울 때는 어떤 기능을 구현해야 할 때, 구현하는 방법을 검색해서 적용하기 때문에 일반적인 use case 를 아는 것이 시작이다. 그리고 use case에 익숙해졌을 쯤에 해당 기능이 어떻게 동작하는지 살피면서 다르게 활용할 수 있는 법을 깨달았을 때 조금 더 배우게 되는 것 같다.
1. 변경은 관리해야 하지만 리렌더링을 발생 시키지 않아도 되는 값을 다룰 때 사용하기
공식 문서를 읽어보면 useRef()는 .current 프로퍼티를 가지고 있고, 변경은 가능하지만 컴포넌트의 전 생애주기를 통해 유지되는 ref 객체를 반환한다고 되어있다. 이게 무슨 말일까?
const
로 선언한 변수 / useState() hook
으로 생성한 변수 / useRef() hook
으로 생성한 변수 의 차이점을 알아보자.
import { useState, useRef } from 'react'
const Component = () => {
const a = 1 // 일반 변수
const [state, setState] = useState() // state 변수
const ref = useRef() // ref 변수
}
컴포넌트의 생애주기란 DOM에 mount 되고 unmount 되기까지의 과정을 말한다. 함수 컴포넌트는 부모로 부터 전달 받는 props가 변경되거나 자신의 state가 변경되면 re-rendering 이 발생하는데, 이 때 내부에서 const
로 선언된 변수는 재선언되고 재할당 된다. 즉 컴포넌트의 생애주기를 통해 유지되지 않고 렌더링 마다 값이 초기화된다. useState() hook
으로 만든 변수는 컴포넌트의 생애주기를 통해 유지되지만 상태값이 변경될 때마다 컴포넌트 리렌더링을 발생시킨다. useRef() hook
으로 만든 변수는 컴포넌트의 생애주기를 통해 유지되지만 .current 프로퍼티의 값이 변경되도 컴포넌트 리렌더링을 발생 시키지 않는다. 예시를 통해 살펴보자
‘hello’ 버튼을 5번 클릭해보고, ‘집사야 눌러봐’ 버튼을 5번 클릭해 본 후, 다시 ‘hello’ 버튼을 1번 눌러보자. hello 버튼을 눌렀을 때는 즉시 리렌더링이 일어나지만 집사야 눌러봐 버튼을 눌렀을 때는 리렌더링이 발생하지 않고 내부적으로 ref.current 값은 계속 업데이트 되다가, 외부적인 이유(ex. state 변경)로 컴포넌트가 리렌더링 되면 그 때 변경된 값이 보여질 뿐이다.
이러한 특성으로 useRef() 로 생성한 ref.current 에 HTMLElement 뿐만 아니라 숫자, 문자열, 배열 등의 값을 할당 할 수 있으며, 컴포넌트 내부에서 변경을 관리해야 하지만 굳이 리렌더링을 발생 시킬 필요는 없을 때 활용할 수 있다.
2. callback ref
.current 프로퍼티값을 변경해도 리렌더링이 일어나지 않는다는 것은 이제 잘 알 것이다. 이 말은 DOM 노드에 <div ref={containerRef}></div>
처럼 useRef() 를 통해 생성한 ref 를 붙이거나 떼어도 컴포넌트는 인지하지 못한다는 것을 의미한다.
컴포넌트가 mount 되었을 때, undefined 이였던 catContainer.current 값이 HTMLDivElement 로 업데이트 되었겠지만 컴포넌트는 인지하지 못하고 있다. 공식 문서에서 설명하는 것처럼 DOM Node에 ref가 attact 되거나 detach 될 때 어떤 코드를 실행하고 싶다면 callback ref 방법을 쓸 수 있다. ref 에 useRef로 반환된 값을 넘기는게 아니라 함수를 넘기는 것 이다.
useRef() hook 으로 ref 객체를 만들 필요 없이 간편하다. 내가 callback ref 방식을 몰랐을 때 특정 DOM Node의 높이를 구하기 위해 아래와 같은 코드를 짜곤 했다. (useState, useRef, useeffect 3개의 hook을 사용해야 했다)
import React, { useState, useRef, useEffect } from "react";
import Cat from "./components/Cat";
import "./styles.css";
export default function App() {
const [height, setHeight] = useState(0);
const catContaierRef = useRef();
useEffect(() => {
setHeight(catContaierRef.current.getBoundingClientRect().height);
// mount 되고 난 뒤의 시점이니까 catContainerRef.current의 값이 업데이트 된 상태
}, [])
return (
<div>
<h4> 고양이가 세상을 구한다 ️</h4>
<p> 내 키는 : {height}px 이야</p>
<div ref={catContaierRef}>
<Cat />
</div>
</div>
);
}
3. forwardRef
상위 컴포넌트에서 하위 컴포넌트에게 props를 통해 데이터를 전달할 수 있듯이, ref도 하위 컴포넌트에게 전달할 수 있다. 다만 일반적인 데이터처럼 props 객체의 프로퍼티로 ref 가 들어가지는 않고, ref 를 넘겨 받는 하위 컴포넌트에서 forwardRef로 함수를 감싸주는 처리를 해줘야 한다.(예시 코드)
* 참고) CodeSandbox 에서 src > components > Cat.js
파일을 확인하세요.
Cat 컴포넌트에서 forwardRef 를 통해 넘겨 받은 값이 undefined이 아니라 HTMLImageElement 인 것을 확인할 수 있다.
여기서 주의할 점은 역시나 부모 컴포넌트에서 ref.current의 값이 HTMLImageElement 로 변경된 것을 인지하지 못한다. 개인적으로 우리가 state를 하위 컴포넌트에게 넘기는 이유는 그 상태값을 상위 컴포넌트에서 관리해야 하기 때문인데, 비슷한 맥락으로 접근했을 때 forwardRef 를 통해 ref를 하위 컴포넌트에게 넘겼을 때 상위 컴포넌트에서 인지하지도 못한다면 굳이 넘겨야 할 필요가 있을까 하는 생각이 든다. (상위 컴포넌트에서 하위 컴포넌트를 div 로 감싸고 그 div에 ref 를 걸거나, 그냥 하위 컴포넌트 내에서 useRef() 로 생성하는 방식으로 대부분 해결할 수 있지 않을까 하는…) 아직 forwardRef 가 확실히 필요한 상황을 마주하지 못했을 수도 있기에, 관련한 배움이 생겨 해당 글을 업데이트 할 수 있으면 좋겠다.
참고한 글
- 리액트 공식 문서
- React Callback Refs — a Complex Case