[React] Hooks - useRef()
회사에서는 Vue를 주로 사용하다보니, 개인공부나 토이 프로젝트에서는 React를 꾸준히 적용하고있다.
이번에 MBTI 사이드 프로젝트를 진행하면서, 페이지 컴포넌트에서 질문 리스트를 불러와서 각 질문들을 인덱스에 따라 렌더링하는 화면을 만들어야했다.
리스트, 인덱스, 질문 데이터들을 모두 useState()에 담아도 당연히 동작은 잘 하겠지만, 리스트 데이터에 대한 고민이 있었다.
첫 번째는, 리스트 데이터는 페이지 리렌더링에 아무런 연관성이 없다.
단순히, 리스트 정보들을 가지고 있고, 질문에 응답한 결과만을 저장하면 되기 때문에 state의 취지에 맞지 않는다고 할 수 있다.
두 번째는, 리스트 데이터가 수정(결과값이 입력)될 때마다 컴포넌트가 리렌더링된다.
각 질문에 대답할때 마다, 질문 리스트의 해당 질문(객체)에 result 프로퍼티를 추가해야한다.
React는 state가 수정될 때마다 리렌더링되기 때문에, 위는 불필요한 리렌더링이므로 최적화에 대해 고민할 수밖에 없었다.
그래서, state가 아닌 일반적인 변수에 리스트를 저장하는 시도도 해보았지만,
여기서의 문제는 컴포넌트가 리렌더링될 때마다 변수가 재선언되고 리스트 객체가 재할당되고 있다는 성능이슈가 있었다.
컴포넌트가 렌더링 & 성능이슈 없이 static 데이터를 어떻게 관리할 지 찾아보았고, useRef() Hooks를 사용해야함을 발견했다.
useRef()는 React DOM을 참조 및 제어하기 위한 Hooks지만, 이외에 다른 목적으로도 유용한 Hooks 였다.
💙 useRef : React 공식문서 (공식문서 링크)
const refContainer = useRef(initialValue);
useRef는 변경 가능한 ref 객체를 반환하며, .current 프로퍼티를 통해 그 값에 접근할 수 있다. initialValue를 넣으면 해당 값으로 초기화한다.
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
보통은 위처럼, DOM에 ref를 설정해서 여기에 .current로 접근하는 Hooks로 사용된다.
DOM의 ref 속성으로 ref 객체를 전달하면, 컴포넌트가 수정될 때마다 변경된 DOM노드를 ref객체의 .current 를 연동하기 때문이다.
* 컴포넌트는 null이었던 nameInput(ref)의 .current가 HTMLDivElement로 업데이트되지만, useRef() 특성상 이를 컴포넌트가 인지하거나 리렌더링이 발생하지 않는 것이다!
- "어떤 가변값을 유지하는데 편리합니다." (공식문서 링크)
공식문서 하단에는 위와 같은 문구가 등장한다. ref 속성보다는, useRef() 자체도 유용한 용도로 사용된다는 것이다.
클래스형 컴포넌트는 고유의 인스턴스 필드 내에서 내부 변수를 관리했다면, 함수형 컴포넌트는 useRef()를 통해 가변값을 관리한다는 내용이다.
useRef()는 순수 자바스크립트 객체를 생성하며, 매번 렌더링할 때 동일한 ref 객체를 참조해서 생성하기 때문에 효율적이다.
또한, ref 객체의 .current 프로퍼티가 변경되어도 컴포넌트는 이를 인지하지 못하기 때문에 리렌더링이 발생되지 않으므로 이를 오히려 전략적으로 사용하는 것이다.
💙 useRef의 용도
1. 특정 DOM 선택
Vanila JS로 개발할 때는 getElementById, querySelector 과 같은 DOM Selector로 엘리먼트의 id 혹은 class에 접근하였다.
React에서도 DOM을 직접 참조하는 케이스가 있다. 스크롤바 위치인식, focus 설정 등의 기능이 대표적이다.
또한, Video 라이브러리, chart.js 등 그래프 라이브러리, Map API 등을 사용할때도 DOM을 선택해서 적용하게된다.
위와 같은 경우에, React 컴포넌트의 ref 속성을 통해 특정 DOM을 참조할 수 있는 좌표를 제공한다.
함수형 컴포넌트에서 ref를 사용할 때 useRef() Hooks를 같이 사용한다. (클래스형 컴포넌트는 콜백함수 or React.createRef)
import React, { useState, useRef } from 'react';
function InputSample() {
const [inputs, setInputs] = useState({ name: '', nickname: '' });
const nameInput = useRef(null);
const { name, nickname } = inputs;
const onChange = e => {
const { value, name } = e.target;
setInputs({ ...inputs, [name]: value });
};
const onReset = () => {
setInputs({ name: '', nickname: '' });
nameInput.current.focus(); // "name" input으로 커서 포커스 이동
};
return (
<div>
<input
name="name"
placeholder="이름"
onChange={onChange}
value={name}
ref={nameInput}
/>
<input
name="nickname"
placeholder="닉네임"
onChange={onChange}
value={nickname}
/>
<button onClick={onReset}>초기화</button>
<div>
<b>값: </b>
{name} ({nickname})
</div>
</div>
);
}
export default InputSample;
예시를 보면, 우선 nameInput 이라는 ref 객체를 useRef()를 통해 생성했다. (null로 초기화)
이 객체를 우리가 선택하고자 하는 DOM의 ref 속성으로 할당하면, ref 객체의 .current 프로퍼티가 해당 DOM을 가르키게 된다.
onReset() 메서드는 입력된 2가지 값(name, nickname)을 초기화하고, 첫 번째 <input>에 포커스를 옮기는 UX를 구현한 코드다.
2. 컴포넌트 안의 변수 관리하기
서론에서도 언급한, useRef() Hooks의 또다른 기능인 컴포넌트 내 조회 및 수정 가능한 변수 관리이다.
ref 객체값이 수정되도 컴포넌트가 리렌더링되지 않기 때문에, 리렌더링 후 조회하는 state와 다른 목적의 데이터 관리에 사용된다.
- setTimeout, setInterval 의 clear를 위한 Id 저장
- 외부 라이브러리를 사용하여 생성된 인스턴스 (그래프 라이브러리, 지도 라이브러리 등)
- scroll 위치
// App.js
import React, { useRef } from 'react';
import UserList from './UserList';
function App() {
const users = [
{
id: 1,
username: 'subin',
email: 'subin@example.com'
},
{
id: 2,
username: 'user1',
email: 'user1@example.com'
},
{
id: 3,
username: 'user2',
email: 'user2@example.com'
}
];
const nextId = useRef(users.length+1);
const onCreate = () => {
// 배열에 새로운 항복 추가하는 로직 생략
nextId.current += 1;
};
return <UserList users={users} />;
}
export default App;
users는 유저들의 데이터 리스트이며, onCreate는 여기에 새로운 유저를 추가하는 로직을 포함한 메서드이다.
각 유저 데이터에는 고유 id값이 할당되는데, 이는 렌더링에는 무관한 데이터이므로 useRef() 를 생성하여 관리한다.
새로운 데이터를 생성할 때마다 nextId.current 값을 id로 할당해주면 되고, nextId.current에 1을 더해 다음 id를 미리 세팅한다.
3. Timer 초기화(clear)
React에선 setTimeout, setInterval 과 같은 Timer 함수들은 필요가 없어질 경우 clear를 시켜주지 않으면 메모리를 많이 소모한다.
통상, useEffect의 cleanup 문법(return)를 적용해서, 컴포넌트가 unmount 될 때 clear 하는 경우가 많다.
const RSPfunction = () => {
const [result, setResult] = useState('');
const [imgCoord, setImgCoord] = useState(rspCoords.바위);
const [score, setScore] = useState(0);
const interval = useRef();
useEffect(() => {
interval.current = setInterval(changeHand, 100);
return () => {
clearInterval(interval.current);
}
}, [imgCoord]);
1회성 타이머는 useEffect 내에서 특정 변수에 setTimer를 할당하고, cleanup에서 clearTimer(변수)를 콜하는 방식을 구현한다.
하지만, 컴포넌트 내에서 반복적으로 사용되어야 할 경우, 이것이 재선언/재할당을 반복되는 비효율성을 없애기 위해 useRef()에 저장한다.
- 사용 예시
아래 코드는, 내가 사이드를 진행하면서 컴포넌트 내에서 useState와 useRef를 나누어서 사용한 케이스이다.
import React, { useState, useRef } from 'react'
import testData from '@/db/testList';
import { arrShuffle } from '@/utils';
import s, { container } from '@/styles/mixin';
import useReactRouter from '@/hooks/useReactRouter';
import TestItemForm, { ScTestItemForm } from '@/views/components/test/TestItemForm';
import TestProgressBar from '@/views/components/test/TestProgressBar';
import { useSetRecoilState } from 'recoil';
import { resultA } from '@/recoil/main';
const TestPage: React.FC = () => {
const { search, navigate } = useReactRouter()
const { phase } = qs.parse(search, { ignoreQueryPrefix: true });
const isPhaseA = phase === 'a'
const themeKey = isPhaseA ? 'green' : 'yellow'
// 1) useRef 사용 부분
const testList = useRef<TestResultList>(arrShuffle([...testData[isPhaseA ? 'phaseA' : 'phaseB']]))
// 2) useState 사용 부분
const [index, setIndex] = useState<number>(0)
const [test, setTest] = useState<TestItem>(testList.current[index])
const setResultA = useSetRecoilState(resultA);
// 3) useRef.current 참조 및 제어 부분
const clickOption = (type: string) => {
testList.current[index].result = type
if (index === testList.current.length - 1) {
setResultA(testList.current)
navigate(isPhaseA ? '/mid-result' : '/result')
}
else {
setIndex(index+1)
setTest(testList.current[index])
}
}
return (
<ScTestPage>
<TestNum themeKey={themeKey}>
Question <span className='num'>{String(index+1).padStart(2,'0')}</span> .
</TestNum>
<TestItemForm test={test} themeKey={themeKey} clickOption={clickOption} />
<TestProgressBar length={testList.current.length} index={index} themeKey={themeKey} />
</ScTestPage>
)
}
export default TestPage
- 1) testList는 리스트 데이터를 저장하고 각 선택의 결과값(result)을 추가하는 변수다. 렌더링과는 무관하므로 useRef를 적용했다.
- 2) test, index는 리스트의 질문과 인덱스로, 인덱스가 변경될 때마다 질문내용, 번호, progress-bar 등이 리렌더되어야 하므로 useState를 사용했다.
- 3) clickOption() 메서드는 질문을 응답했을 때 발생한다. testList의 현재질문(.current)에 result를 업데이트하고, 조사가 종료되면(index와 testList 길이 비교) store에 결과를 세팅하고 다음 페이지로 넘기며, 아니라면 인덱스를 1 더하고 다음 질문으로 넘긴다.
useRef는 다른 Hooks에 비해 사용빈도가 매우 높은 편은 아니라고 한다. 하지만, 사용목적은 매우 명확한 Hooks라고 생각한다.
DOM의 직접 선택은 focus 제어, 외부 라이브러리 적용 등에 필수적이며, 예시처럼 렌더링과 무관한 데이터 저장에도 유용하게 사용될 수 있다.
- [React 공식문서] Hooks API 참고서(useRef 포커스) : https://ko.reactjs.org/docs/hooks-reference.html#useref
- [useRef 특정 DOM 선택] 벨로퍼트 님의 블로그 : https://react.vlpt.us/basic/10-useRef.html
- [useRef 컴포넌트 내 변수] 벨로퍼트 님의 블로그 : https://react.vlpt.us/basic/12-variable-with-useRef.html
- [useRef 사용용도] 박윤종 님의 블로그 : https://yoonjong-park.tistory.com/entry/React-useRef-%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80