React ref, forwardRef, useImperativeHandler 이해하기
React로 개발하다보면 DOM 객체에 접근해야할 때 ref를 사용한다.
클래스형 컴포넌트에서 React.createRef()
를 사용해서 접근했다면, 함수형 컴포넌트에서는 useRef()
훅을 통해 ref 설정을 할 수 있다.
ref 설정
함수형 컴포넌트 기준, ref는 useRef()를 사용하여 선언하고 초기화한다.
useRef()
통해 생성하면 내부에 current
라는 변수를 가지고 있는 객체로 생성
이 되며, ref에 할당되는 값은 current
변수에 저장이되므로 항상 참조할 때 current
값의 존재 여부를 확인하고 사용해야한다.
const ref = useRef(0);
// {
// current: 0
// }
여기서 중요한 점은 useRef()는 객체를 반환한다는 점이다.
자바스크립트에서 전역변수와 참조타입의 변수는 heap
영역에 저장되고, 사용되지 않는 변수는 가비지 컬렉터를 이용해 삭제된다.
즉, 매번 렌더링할 때 동일한 객체(동일한 참조값)을 제공하여 값이 변경된다하더라도 같은 메모리 주소를 갖고 있기 때문에 자바스크립트의 동등 비교 연산이 항상 true를 반환한다.
따라서 변경 사항을 감지할 수 없고, 리렌더링이 되지 않는다.
ref로 값 참조
화면이 렌더링되기 위해서는 두 단계로 나뉘어진다.
- render phase : 렌더링 하는 동안 React는 컴포넌트를 호출하여 화면에 변경된 내용을 파악한다.
- commit phase : 커밋하는 동안 React는 DOM에 변경 사항을 적용한다.
render phase
에서는 DOM 노드가 아직 생성되지 않았으므로 ref.current의 값은 null
이 된다.
commit phase
에서는 ref.current를 설정한다. 즉, React는 DOM이 업데이트 되기 전에는 ref.current = null
로 설정했다가, DOM이 업데이트된 직후 ref.current 값을 DOM노드로 설정
한다.
따라서 일반적으로는 이벤트 핸들러에서 ref를 사용하며, ref로 특정 작업을 수행할 이벤트가 없다면 useEffect 내부에서 사용할 수 있다.
forwardRef
함수 컴포넌트에서 커스텀 컴포넌트에 ref를 props로 전달하려고 하면 기본적으로 null이 반환된다. 자식 컴포넌트 DOM에 접근 할 수 없다.
import { useRef } from 'react';
function MyInput(props) {
return <input {...props} />;
}
export default function MyForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
이유는 React에서 기본적으로 컴포넌트가 다른 컴포넌트의 DOM에 접근하는 것을 허용하지 않기 때문이다. 심지어 자신의 자식 컴포넌트도 포함이다.
자식 컴포넌트에 접근하기 위해 forwardRef
를 사용하면 외부에서 전달 받은 ref를 컴포넌트 내부에 전달할 수 있다.
사용법은 다음과 같다.
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
...
useImperativeHandler
이전 기업 프로젝트로 돌림판 돌리기를 구현할 때 사용한 경험이 있다.
부모 컴포넌트에서 돌림판 돌리기 버튼을 클릭했을 때 자식 컴포넌트인 돌림판을 돌아가는 함수를 동작시켜야하는 상황이었다.
useImperativeHandler()
은 리액트 훅 중 하나로, 부모 컴포넌트에게 노출할 ref 핸들러를 사용자가 직접 정의(ref로 노출 시키는 노드의 일부 메서드만 노출)할 수 있게 해주는 훅이다.
따라서 자식 컴포넌트에 정의한 핸들러를 부모 컴포넌트에 노출시켜 사용할 수 있다.
import {
forwardRef,
useRef,
useImperativeHandle
} from 'react';
const Roulette = React.forwardRef((props, ref) => {
const { items } = props;
const refs = useRef();
...
// 핸들러를 부모 컴포넌트에 노출시킵니다.
useImperativeHandle(ref, () => ({
handleRotateRoulette,
}));
const handleRotateRoulette = () => {
rotation.value = withTiming(rotationAngle, {
duration: 5000,
easing: Easing.inOut(Easing.circle),
});
};
...
// components/Roulette/form 부모 컴포넌트
const RouletteForm = ({ navigation, route }) => {
const rouletteRef = useRef();
...
const handleSubmitVote = () => {
if (isVote) setVoteText('원판 돌리기');
else {
rouletteRef.current.handleRotateRoulette();
}
};
...
return (
...
<Roulette
ref={rouletteRef}
...
/>
...
);
};
참고
- React 공식문서(ko) - ref로 값 참조하기 (opens in a new tab)
- React 공식문서(ko) - ref로 DOM 조작하기 (opens in a new tab)
- React 공식문서 - useImperativeHandle (opens in a new tab)
- useRef()가 순수 자바스크립트 객체라는 의미를 곱씹어보기 (opens in a new tab)
- React ref와 forwardRef 그리고 useImperativeHandle 제대로 알기 (opens in a new tab)
- [React] - useRef (opens in a new tab)