이번 시간에는 useRef hook을 배워봅시다. 여기까지가 자주 쓰이는 훅인 것 같습니다. useContext, useReducer, useImperativeHandle, useLayoutEffect도 리액트의 기본 훅이지만 상대적으로 사용 빈도가 떨어집니다. useContext와 useReducer는 제 유튜브 무료 강좌에서 설명해놓긴 했습니다.
컴포넌트 별로 데이터를 갖고 싶을 때가 있습니다. useState를 쓰면 되지 않냐고요? useState도 데이터를 저장하지만 제가 useState를 설명할 때 한 가지 단서를 달았습니다. 바로 화면 렌더링과 관련된 데이터를 저장하는 공간이라고요. state를 바꾸면 컴포넌트가 리렌더링되고 맙니다. 하지만 만약 화면 리렌더링과는 관련 없는 데이터를 저장하고 싶다면 어떻게 해야 할까요?
직관적으로 떠오르는 방법은 두 개가 있습니다. 컴포넌트 바깥에 데이터를 놓는 것과, 컴포넌트 안에 데이터를 넣는 것이죠.
import React, { useCallback } from 'react';
let data = 0;
const Basic = () => {
const onClick = useCallback(() => {
data++;
}, [data]);
return <div onClick={onClick}>Basic</div>;
};
export default Basic;
이 방법은 누구나 생각할 법 하지만 한 가지 명심해야할 점이 있습니다. 만약 Basic 컴포넌트를 여러 군데서 사용한다면 그 컴포넌트들이 모두 data라는 데이터를 공유한다는 것입니다.
<Basic />
<Basic />
즉 위과 같은 상황에서 위와 아래의 Basic을 한 번씩 누른다면 data는 2가 되어 있습니다. 컴포넌트 간에 공유하고 싶은 데이터는 이렇게 만드시면 됩니다.
만약 공유하기 싫고 각각 데이터를 갖길 원한다면
import React, { useCallback } from 'react';
const Basic = () => {
let data = 0;
const onClick = useCallback(() => {
data++;
}, [data]);
return <div onClick={onClick}>Basic</div>;
};
export default Basic;
이렇게 안에 쓰는 걸 생각해볼 수도 있을 것입니다. 하지만 이것은 제대로 동작하지 않습니다. 컴포넌트가 리렌더링될 때 let data = 0;이 다시 실행되어 data가 0으로 초기화되어 버리기 때문이죠. 리렌더링과 관련없이 한 번 만들어진 컴포넌트에서 이전 데이터를 유지하고 싶다면 어떻게 해야 할까요? 이 때 useRef 훅이 나옵니다.
import React, { useCallback, useRef } from 'react';
const Basic = () => {
const dataRef = useRef(0);
const onClick = useCallback(() => {
dataRef.current++;
}, []);
return <div onClick={onClick}>Basic</div>;
};
export default Basic;
useRef로 생성한 데이터는 리렌더링 여부와 상관없이 같은 값이 유지됩니다. 또한 그 값을 바꾸더라도 화면이 리렌더링되지 않습니다. 초깃값은 useRef로 생성할 때 인수로 넣어주면 됩니다. 여기서 특이한 점은 dataRef의 값을 가져올 때 dataRef.current로 접근해야 한다는 것입니다. dataRef 자체에 대입하면 안 되고 항상 current 속성을 사용해 접근하도록 하세요.
useRef는 class 컴포넌트에서 사용했던 React.createRef와 마찬가지로 DOM 노드를 저장하는 데 사용할 수도 있습니다. ref props로 넣고 나중에 divRef.current.focus()같이 DOM에 직접 접근하면 됩니다.
import React, { useRef } from 'react';
const Basic = () => {
const divRef = useRef(null);
return <div ref={divRef}>Basic</div>;
};
export default Basic;
useRef를 활용한다면 useEffect에서 componentDidUpdate 효과를 낼 수 있습니다. componentDidMount를 무시하는 방법입니다.
import React, { useEffect, useRef } from 'react';
const Basic = () => {
const mountRef = useRef(false);
useEffect(() => {
if (mountRef.current) {
console.log('updated!');
} else {
mountRef.current = true;
}
});
return <div>Basic</div>;
};
export default Basic;
useEffect는 mount될 때도 한 번 실행되는데 그 때는 mountRef가 false입니다. 따라서 if 문이 동작하지 않습니다. 단, else문에서 mountRef를 true로 바꾸어놨기 때문에 다음 리렌더링때부터는 if문 내부가 실행됩니다.
정리하자면 useRef는 그 안의 데이터가 바뀌어도 화면을 리렌더링하지 않지만, 리렌더링 후에도 데이터를 유지시켜줍니다. 클래스 컴포넌트의 React.createRef에 비해서 활용도도 높습니다.
다음 시간에는 hook의 규칙과 커스텀 훅에 대해 알아보도록 하겠습니다.