💡 중요
"useRef는 .current 프로퍼티로 전달된 인자 (initialValue)로 초기화된 변경 가능한 ref 객체를 반환한다. 반환된 객체는 컴포넌트의 전 생애 주기를 통해 유지될 것이다 "(출처: 공식문서)useRef는 .current 프로퍼티에 변경 가능한 값을 담고 있는 '상자'이다.
useRef는 current 속성을 가지고 있는 객체를 반환하고, 그 객체는 컴포넌트 전 생애주기 동안 유지된다. 따라서 current의 값이 변경되어도 리렌더링이 이루어지지 않는다.
즉, Ref는 변경되어도 리렌더링이 되지 않고, 리렌더링이 되어도 Ref는 유지된다.
작동 방식 예: https://charles098.tistory.com/184
ref는 JS의 getElementById()와 같이 컴포넌트의 어떠한 부분을 선택할 수 있게 한다. 가장 많이 쓰이는 예로는 input태그에 자동으로 마우스 포커싱이 되어 별도로 마우스 이동이 필요없는 경우이다.
const inputRef = useRef();
<input value="아이디" inputRef={inputRef}/>
useEffect(() => {
inputRef.current.focus();
}, []);
💡 중요
실제로 내가 진행하고 있는 프로젝트에서 useRef를 사용해야 하는 방법 중 이러한 상황이 있었다.
네비게이션바의 메뉴를 클릭하면 해당 페이지로 스크롤링되는 것이었다. 따라서, 스크롤되어야 하는 각각의 컴포넌트마다 ref객체를 생성하여 scrollIntoView()를 통해 페이지 스크롤 기능을 구현하였다.
하지만, 각각의 ref를 하나씩 다 생성하는 것은 비효율적이다.
따라서, 접근하고 싶은 여러개의 DOM을 하나의 객체로 관리하는 것이다.✏️ 1. useRef 객체를 생성할 때 빈 배열로 초기화
const scrollRef = useRef([]);
✏️ 2. 각각의 DOM마다 배열의 인덱스를 설정.
<PageScroll1 ref={el => scrollRef.current[0] === el}/> <PageScroll2 ref={el => scrollRef.current[1] === el}/>
✏️ 3. 클릭 시 index값과 일치한 컴포넌트로 scrolling
const handleIndexClick = index => { scrollRef.current[index].scrollIntoView({ behavior: 'smooth', block: 'end', });
변수를 저장하고 관리하기 위한 방법으로는 대략적으로 세 가지가 있다.
1. 일반 변수에 저장
var countVar = 0;
2. useState의 상태값에 저장
const [countstate, setCountState] = useState(0);
3. useRef의 current의 초기값으로 저장
const countRef = useRef(0);
일반 변수에 저장한 경우에는 변수값에 대한 변화가 일어나도 화면단에는 아무런 변화가 없다. state또는 props의 값 변경이 아니기 때문에 리렌더링이 일어나지 않기 때문이다.
state에 저장한 경우에는 상태가 변하므로 변경이 될때마다 리랜더링이 일어난다. 이때 랜더링으로 인해서 일반 변수에 대한 값들은 랜더링으로 인해 초기화된다.
그렇다면 값의 초기화를 막고 싶고, 쓸데없이 상태 변경마다 랜더링 되는 것을 막고 싶다면?
그래서 사용할 수 있는 것이 useRef이다. 앞서 말했듯이 Ref 객체를 생성할 경우 current에 값이 저장되고 이 값은 전 생애주기동안 유지된다. 즉 리랜더링이 일어나더라도 값이 유지된다는 것이다.
하지만, current의 값은 리랜더링이 되어도 유지되듯이 current의 값이 변경되어도 리랜더링은 이루어지지 않는다.
화면에 해당 값이 출력되는 것을 확인하려면 랜더링이 되어야하기 때문에 값의 변화를 확인할 수 없는 것이다. 따라서, Ref값이 1씩 증가하는 버튼을 10번을 클릭하여도 화면에는 값에 대한 변화를 확인할 수 없다. 그러다가 랜더링을 발생시켰을 경우 0에서 10으로 변경되어 있는 것을 확인할 수 있다.
useRef의 변수관리에 대한 대표적인 예로 setTimeout과 setInterval를 확인해보자.
✏️ setTimeout/setInterval에서의 useSate 작동방식
const [number, setNumber] = useState(0)
let timer = setInterval(()=> {
setNumber(number + 1)
console.log(number)
}, 1000)
// 0 loop
useState는 prevState와 nextState를 비교하고 변경된다. 이때 setInterval 내부 callback의 closure에는 prevState가 참조된다. 참조로 인하여 prevState는 메모리에 남게되고 garbage collector의 영향을 받지 않는다. 따라서 setState로 state를 변경하더라도 state값은 변경되지 않게 된다.
💡 잠깐) garbage collector?
setInterval 또는 setTimeout은 clear 시키지 않으면 메모리를 많이 소모하게 된다. 따라서,꼭 봐야하는 사이트
✏️ setTimeout/setInterval에서의 useRef 작동방식
const number = useRef(0)
let timer = setInterval(()=> {
number.current = number.current + 1
console.log(number)
}, 1000)
// 0, 1, 2, 3, 4
useRef는 plain JS Object(순수JS객체)를 생성한다. 즉 useState처럼 비교하는 과정이 없고 current의 변경이 있을 시 JS객체도 그대로 변경되어진다. 이때 setInterval의 내부 callback의 closure에는 current값이 참조되어 값이 출력된다.
✏️ 만약 setInterval/setTimeout에 useSate 값을 사용하고 싶다면?
const number = useRef(0)
const [numberState, setNumberState] = 0
let timer = setInterval(()=> {
number.current = number.current + 1
setNumberState(number.current)
console.log(number)
}, 1000)
// 0, 1, 2, 3, 4
참조
쉬운설명: