useRef
2023-09-17
ReactHooks
Table of Contents
useRef가 나오게 된 배경
- React에선 DOM에 직접 수정하는 것을 지양합니다. (document.querySelector 등..) 가상 DOM을 사용하기 때문입니다.
- 가상 DOM이 아닌 실제 DOM을 제어하게 된다면, React의 구조에서 벗어나게 되고, 예상치 못한 side Effect를 유발합니다.
- useState에 DOM 상태 값을 관리하게 된다면 불필요한 많은 리렌더링이 발생하게 됩니다.
- DOM의 상태를 저장하는데, re-rendering이 안되게 해야 할 때 쓰는 Hook. 여기서 비제어, 제어 컴포넌트의 개념이 나오게 됩니다.
제어, 비제어 컴포넌트
- 제어 컴포넌트는 사용자가 입력한 값과 저장되는 값이 실시간으로 동기화됩니다(useState). 반대로 비제어 컴포넌트는 사용자가 입력한 값을 실시간으로 동기화하지 못합니다. 리액트에서 감지하지 못하기 때문이죠.
- 왜 감지하지 못하나요?
- useRef() 는 heap영역에 저장되는 일반적인 자바스크립트 객체입니다.
- 매번 렌더링할 때 동일한 객체를 제공한다. heap에 저장되어 있기 때문에 애플리케이션이 종료되거나 GC될 때 까지, 참조할때마다 같은 메모리 값을 가진다고 할 수 있습니다.
- 값이 변경되어도 리렌더링이 되지 않습니다. 같은 메모리 주소를 갖고있기 때문에 자바스크립트의 연산이 항상 true 를 반환합니다. 즉 변경사항을 감지할 수 없어서 리렌더링을 하지 않는다는 뜻입니다.
- 왜 감지하지 못하나요?
1. 프로젝트 세팅 React + TS + Vite
$ npm init vite@latest React-UseRef -- --template react-ts
$ cd React-UseRef
$ npm i
2. useRef 사용해보기
1. 저장공간으로 활용
/src/Ref.tsx
import { useRef, useState } from 'react';
const RefCounter = () => {
// rendering trigger
const [, setRender] = useState(false);
// ref
const ref = useRef<number>(0);
// 일반 변수
let val = 0;
// ref 및 변수 change
function valueChange() {
// 상태값 저장도 할 수 있다.
console.log('handle click useRef ::: ' + ref.current);
ref.current = ref.current + 1;
console.log('handle click 일반변수 ::: val ' + val);
// val에 1더하기
val++;
alert('일반 변수 val = ' + val + ' useRef.current = ' + ref.current);
}
console.log('Ref Component Update', val, ref.current);
// 강제 렌더링
function render() {
setRender((prev) => !prev);
}
return (
<>
<button onClick={valueChange}>Click Changed Ref Value!</button>
<button onClick={render}>Force Rendering!</button>
</>
);
};
export default RefCounter;
- 인자로 넘어온 초깃값을 useRef 객체의 .current 프로퍼티에 저장합니다.
/src/App.tsx
import "./App.css";
import RefCounter from "./Ref";
function App() {
return (
<>
<RefCounter />
</>
);
}
export default App;
- ref의 값이 나오게되고, 값은 일반변수도 같이 증가합니다.
- useState와 다르게 렌더링을 하지않아 함수가 다시 실행되지 않고, 일반변수의 값이 초기화되지 않아 저장됩니다.
2. DOM 연결하기
/src/App.tsx
import { useRef, useState } from "react";
import "./App.css";
function App() {
const inputRef = useRef<HTMLInputElement>(null);
const [state, setState] = useState("");
// input 체인지 이벤트 발생 시 실행할 함수
function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault();
setState(event.target.value);
}
console.log("DOM UPDATE!");
return (
<>
<h4>제어 컴포넌트</h4>
<input type='text' onChange={onChangeHandler} />
<div>useState state 값 ::: {state}</div>
<hr />
<h4>비제어 컴포넌트</h4>
<div>Ref는 렌더링이 되지 않으면 확인할 수 없습니다.</div>
<input type='text' ref={inputRef} />
<div>useRef current.value 값 ::: {inputRef.current?.value}</div>
</>
);
}
export default App;
- 제어 컴포넌트와 비제어 컴포넌트는 React 입장에서 제어할 수 있냐, 없냐라는 뜻입니다. Ref를 사용하게 된다면 React의 제어에서 벗어나게 됩니다.
- 인자로 넘어온 초깃값을 useRef 객체의 .current 프로퍼티에 저장합니다. 이 방식에서는 useRef의 current 객체에 input값이 담겨 있겠죠.
- useState가 리렌더링을 발생시키면 useRef에 할당된 값이 나오게됩니다. 값은 저장하지만, 렌더링은 발생시키지 않습니다.
3. Update되는 부모 컴포넌트에서 Ref의 동작
/src/Ref.tsx
import { useRef,
// useState
} from 'react';
const RefCounter = () => {
// rendering trigger
// const [, setRender] = useState(false);
// ref
const ref = useRef<number>(0);
// 일반 변수
let val = 0;
// ref 및 변수 change
function valueChange() {
// 상태값 저장도 할 수 있다.
console.log('handle click useRef ::: ' + ref.current);
ref.current = ref.current + 1;
console.log('handle click 일반변수 ::: val ' + val);
// val에 1더하기
val++;
alert('일반 변수 val = ' + val + ' useRef.current = ' + ref.current);
}
console.log('Ref Component Update', val, ref.current);
// 강제 렌더링
// function render() {
// setRender((prev) => !prev);
// }
return (
<>
<button onClick={valueChange}>Click Changed Ref Value!</button>
{/* <button onClick={render}>Force Rendering!</button> */}
</>
);
};
export default RefCounter;
- 렌더링이 트리거 되는 부분은 주석처리하겠습니다.
/src/App.tsx
import { useRef, useState } from "react";
import "./App.css";
import RefCounter from "./Ref";
function App() {
const inputRef = useRef<HTMLInputElement>(null);
const [state, setState] = useState("");
// input 체인지 이벤트 발생 시 실행할 함수
function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault();
setState(event.target.value);
}
console.log("DOM UPDATE!");
return (
<>
<h4>제어 컴포넌트</h4>
<input type='text' onChange={onChangeHandler} />
<div>useState state 값 ::: {state}</div>
<hr />
<h4>비제어 컴포넌트</h4>
<div>Ref는 렌더링이 되지 않으면 확인할 수 없습니다.</div>
<input type='text' ref={inputRef} />
<div>inputRef state 값 ::: {inputRef.current?.value}</div>
<RefCounter />
</>
);
}
export default App;
- useState는 컴포넌트를 Updating 시킵니다. 그 과정에서 자식 컴포넌트들도 re-rendering 과정을 거치게 되는데요.
- 자식 컴포넌트의 일반변수 val의 값은 초기화되고, Ref의 값은 저장되어 있습니다. 부모 컴포넌트가 Updating 되어도 자식 컴포넌트의 Ref값은 re-rendering이 되어도 값을 저장할 수 있다는 것을 확인할 수 있습니다.
3. 어떻게 써야할까?
Ref를 써야 할 경우
DOM 노드, 엘리먼트, React 컴포넌트 주소값을 활용하는 경우
<input>-focus()- media playback -
play(),pause(),remove() - text selection
- 애니메이션 적용
- d3.js, greensock 등 DOM 기반 라이브러리 활용
State를 써야 할 경우
유저 데이터 실시간으로 받아와야 하는 경우
- 유효성 검사
- 조건문으로 버튼의 활성화 여부를 판단할 때
TypeScript에서의 useRef 타입에 따른 동작 방식
1. 값의 저장방식으로 사용
const localValRef = useRef<number>(0);
-
useRef는 T타입을 제네릭으로 할당하고, 초기값은 T타입이며, MutableRefObject에 T타입을 할당하여 리턴합니다.
- MutableRefObject는 T타입의 current 프로퍼티를 반환합니다.
2. DOM에 연결방식으로 사용
const inputRef = useRef<HTMLInputElement>(null);
- useRef가 T타입 혹은 null 타입을 받을때 RefObject에 T타입을 할당하여 리턴합니다.
- RefObject는 readonly 속성이 들어간 T 혹은 null 타입 current 프로퍼티를 반환하는데, 실제 React의 DOM을 받아봤을때, 읽기 전용 속성인데 어떻게 수정을 할 수 있지? 라고 조금 헷갈릴 수 있습니다. React DOM을 할당한 Ref객체는 current객체를 수정하는 것이 아닌, current.value를 수정하기 때문에 readonly 속성을 벗어날 수 있습니다.
const localVarRef = useRef<number>(null);
- 이 경우가 null이 할당된 경우입니다. 읽기 전용 속성은 수정할 수 없으므로 수정 로직 작성 시 타입에러를 리턴합니다.
인용 및 유용한 글들
https://react.dev/reference/react/useRef#examples-dom
https://velog.io/@xmun74/useRef