먼저 뜻이 어디서 파생되었는지 알아보았다. reference의 준말이라고 한다. 한국말로는 참조, 참고 정도의 뜻을 지닌다고 보면된다.
일반적으로 HTML에서 DOM요소에 이름을 달때는 id라는 고유값을 사용한다. 이처럼 리액트에서도 DOM요소에 이름표를 붙이는 방법이 있는데 이를 ref라고 부른다.
Ref는 사실 일반 객체이다. Ref를 console.log로 찍어보면 {current: null} current 프로퍼티 하나를 가진 객체가 나타나고 React는 이 객체를 통해 DOM에 직접적인 접근을 가능하게 해준다.
HTML에서 DOM요소에 이름을 달때 쓰이는 id는 유일해야 하기때문에 컴포넌트 재사용을 한다면 중복될 가능성이 농후하다. 반면에 Ref는 전역적으로 작동하지 않고 컴포넌트 내부에서만 작동한다.
React에서 state로만 해결할 수 없고, DOM을 반드시 직접 건드려야할때 사용한다.
ex) 특정 input에 포커스 주기, 스크롤 박스조작, 애니메이션을 직접적으로 실행시킬때,서드 파티 DOM 라이브러리를 React와 같이 사용할 때
위의 경우들을 비제어컴포넌트를 제어할때 라고 일축할 수 있을것 같다. React 시스템 안에서 제어하지 않고, 순수 JS를 이용해 제어하는 컴포넌트를 비제어 컴포넌트라고 한다. 말이 조금 어렵지만, React가 제어하지 않는 컴포넌트라고 생각하면 될거 같다. 비제어 컴포넌트를 순수 JS로 제어하기 위해서 DOM 요소를 담는 역할을 Ref가 하는것이다.
React 공식문서에서는 위의 경우가 아닌 선언적으로 해결될 수 있는 문제에서는 ref 사용을 지양하고 있다.
그냥 DOM요소를 반환해주면 좋을 텐데 왜 createRef/useRef는 왜 객체를 반환하고 current 프로퍼티로 DOM 요소를 전달할까?
먼저 React가 가상 돔을 기반으로 작동하는 라이브러리라는 사실을 생각해봐야 합니다.
컴포넌트가 마운트될때 React는 current 프로퍼티에 DOM 엘리먼트를 대입하고, 컴포넌트의 마운트가 해제될때 current 프로퍼티를 다시 null로 돌려놓습니다. ref를 수정하는 작업은 componentDidMount 또는 componentDidUpdate 생명주기 메서드가 호출되기 전에 이루어집니다.
실제 DOM에 React 노드가 렌더될 때가지 Ref가 가리키는 DOM 요소의 주소값은 확정된 것이 아니다.우리가 Ref에 접근할 수 있는 시점은 React노드가 실제로 DOM에 반영되는 시점부터이다.
useref는 초깃값을 따로 설정할 수 있고 null값으로 해놓으면 라이프사이클상에서는 componentDidMount 그 이전에는 null이 current 프로퍼티에 담깁니다.그리고 가상 DOM이 변경될 때 실제 DOM의 요소도 변경되는 경우가 있기 때문에 DOM이 업데이트되는 경우(componentDidUpdate)도 ref의 current 값이 변경되게 됩니다.
이처럼 유동적이기에 React는 객체를 반환해 current 프로퍼티의 값을 계속해서 수정합니다.
저장공간이라 하면 보통 State가 떠올를 텐데 State의 값을 바꿀 때 대표적으로 hooks의 useState를 이용합니다. React 컴포넌트는 State가 변할 때마다 다시 렌더링이 되면서 컴포넌트 내부 변수들이 초기화가 됩니다. 컴포넌트 내부 변수들이 초기화가 된다는 것은 해당 컴포넌트 함수의 변수들이 모두 초기화가 되고 모든 함수 로직 등이 다시 실행되는 것을 의미합니다. 이렇게 원하지 않는 렌더링 때문에 곤란할 때가 있습니다. 그렇다면 State대신 Ref안에 값을 저장하면 어떻게 될까요? Ref의 유용한 점은 Ref안에 있는 값을 아무리 변경해도 컴포넌트는 다시 렌더링 되지 않습니다. 즉, State 대신 Ref를 사용한다면 불필요한 렌더링을 막을 수 있습니다. 또한 컴포넌트가 아무리 렌더링이 되어도 Ref안에 저장되어 있는 값은 변화되지 않고 그대로 유지가 됩니다. 그렇기 때문에 변경 시 렌더링을 발생시키지 말아야 하는 값을 다룰 때 정말 편리합니다.
State의 변화 ➡️ 렌더링 ➡️ 컴포넌트 내부 변수들 초기화
Ref의 변화 ➡️ No 렌더링 ➡️ 변수들의 값이 유지됨
State의 변화 ➡️ 렌더링 ➡️ 그래도 Ref의 값은 유지됨
대표적으로는 input요소를 클릭하지 않고 포커스를 주고 싶을 때 많이 사용됩니다. 예를 들어, 로그인 화면이 보여줬을 때 id를 넣는 Input을 굳이 클릭하지 않아도 자동적으로 포커스가 되어 있게 해 주면 바로 키보드를 입력해서 id를 입력할 수 있어 굉장히 편리할 수 있습니다. 바닐라 자바스크립트의 document.querySelector()와 비슷하다고 생각하시면 됩니다.
[Import]
react Hooks의 useRef를 사용하기 위해서는 react에서 useRef를 import 받아야 합니다.
import { useRef } from 'react';
[예제 - 1. 변수 관리]
useRef를 이용하여 컴포넌트가 아무리 렌더링이 되어도 Ref안에 저장되어 있는 값은 변화되지 않고 그대로 유지가 되는지 확인하는 예제입니다. 즉, 변수 관리를 어떻게 하는지 확인해보도록 하겠습니다.
import { useState, useRef } from 'react'
import './App.css';
function App() {
const [render, setRender] = useState(false);
const countRef = useRef(0);
let countVar = 0;
console.log('***** 렌더링 후 Ref:', countRef.current);
console.log('***** 렌더링 후 Var:', countVar);
const increaseRef = () => {
countRef.current = countRef.current + 1;
console.log('Ref Up! --->', countRef.current);
}
const increaseVar = () => {
countVar = countVar + 1;
console.log('Var Up! --->', countVar);
}
const doRender = () => {
setRender(!render);
}
return (
<div className="App">
<header className="App-header">
<p>Ref: {countRef.current}</p>
<p>Var: {countVar}</p>
<div>
<button onClick={increaseRef}>Ref Up!</button>
<button onClick={increaseVar}>Var Up!</button>
<button onClick={doRender}>Render!</button>
</div>
</header>
</div>
);
}
export default App;
[실행 화면]
Ref Up과 Var Up 버튼을 클릭하면 콘솔에 각각의 값들이 증가하는 것을 확인할 수 있습니다. 하지만, Render 버튼을 클릭하지 않으면 화면에 업데이트가 되지 않습니다. Ref의 값이 5이고 Var의 값이 4일 때 Render버튼을 클릭하였는데 화면에 Ref는 5가 잘 표시되었지만, Var는 0으로 표시되었습니다. 렌더링이 될 때마다 컴포넌트 함수 내부에 있는 변수들이 다시 초기화가 되어 Var의 값은 0이 됩니다. 하지만, Ref는 다릅니다. 아무리 컴포넌트가 렌더링이 되어도 계속 값을 유지합니다. 왜냐하면 Ref의 값은 컴포넌트의 전 생애주기를 통해 유지되기 때문입니다. 컴포넌트가 브라우저의 마운팅 된 시점부터 마운트 해제될 때까지 같은 값을 계속해서 유지할 수 있다는 뜻입니다.
[예제 - 2. DOM요소에 접근]
로그인 화면이 보여줬을 때 id를 넣는 Input을 굳이 클릭하지 않아도 자동적으로 포커스가 되어 있게 해 주면 바로 키보드를 입력해서 id를 입력할 수 있도록 하는 예제입니다.
import { useEffect, useRef } from 'react'
import './App.css';
function App() {
const inputRef = useRef();
useEffect(() => {
console.log(inputRef);
inputRef.current.focus();
}, [])
const loginAlert = () => {
alert(`환영합니다. ${inputRef.current.value}`);
inputRef.current.focus();
}
return (
<div className="App">
<header className="App-header">
<input ref={inputRef} type="text" placeholder="id"/>
<button onClick={loginAlert}>Login</button>
</header>
</div>
);
}
export default App;
[실행 화면]
페이지가 렌더링 될 때도 id input창에 포커스가 잘 되어 있고, 환영한다는 앨럿의 확인을 누르고 난 뒤에도 input으로 포커스가 잘 된 것을 확인하실 수 있습니다.