도대체 왜 내가 원하는대로 스탑워치가 작동을 안하지.. 싶었는데, 제가 리액트의 Hooks와 리렌더링에 대한 이해가 매우매우 부족해서 일어난 상황이라는 것을 깨닫고 빠르게 글을 작성해보려고 합니다.
(본 글에서는 hooks인 useRef를 다루므로 createRef의 내용은 생략합니다!)
목차
0. useState와 useRef
둘의 공통점은 함수형 컴포넌트에서 상태관리를 도와주는 hooks라는 점입니다.
다만 useState와 useRef의 가장 큰 차이점은 렌더링 여부에 있습니다.
useState는 값이 바뀔 때마다 리렌더링이 발생하는데, useRef는 값이 바뀌더라도 리렌더링이 발생하지 않죠.
더 확실하게 알아보기 위해서 공식 문서를 확인해보겠습니다.
useRef() is useful for more than the ref attribute. It's handy for keeping any mutable value around similar to how you'd use instance fields in classes.
The useRef() Hook isn't just for DOM refs. The "ref" object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class.
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
Keep in mind that useRef doesn't notify you when its content changes. Mutating the .current property doesn't cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
이것은 useRef의 객체 참조 특징때문인데, useRef는 하나의 객체 안에서 .current를 통해서 데이터를 관리합니다.
결국 내부의 값이 변하더라도 참조형 데이터의 특성상 주소값은 변하지 않습니다.
따라서 변경사항을 감지하지 못하기 때문에 리렌더링을 하지 않습니다.
1. 예시
아래는 제가 만들고 있는 스탑워치 애플리케이션에서 발생한 문제 예시입니다.
(코드 전체가 아닌 필요한 부분만 가져왔기에 아래 코드만으로는 정상적으로 작동하지 않습니다.)
isPressed와 speed는 렌더링과 전혀 관계가 없는 변수임에도 useState로 관리를 하다보니 매번 값이 바뀔 때마다 리렌더링이 되는 현상이 발생했습니다.
그래서 제가 원하는 동작인 꾹 눌렀을 때 점차 증가 속도가 빨라지는 결과를 얻을 수 없었습니다.
(마우스를 뗐는데도 계속해서 console.log가 출력되는 현상이 발생했습니다..)
const Stopwatch = () => {
const [time, setTime] = useState(0);
const [isPressed, setIsPressed] = useState(false);
const [speed, setSpeed] = useState(300);
const onIncrease = () => {
setIsPressed(true);
repeatCount(1);
};
const offPress = () => {
setIsPressed(false);
setSpeed(300);
};
const onDecrease = () => {
setIsPressed(true);
repeatCount(-1);
};
const repeatCount = (cnt) => {
console.log("ww");
console.log("상태", isPressed);
if (isPressed) {
if (time > 0) setTime(time + cnt);
if (speed > 100) setSpeed(speed - 10);
setTimeout(() => {
repeatCount(cnt);
console.log("w2w");
}, 300);
}
};
return (
<div css={watchContainer}>
<img
src={Plus}
alt="더하기"
onMouseDown={onIncrease}
onMouseUp={offPress}
></img>
<input type="text" value={time}></input>
<img
src={Minus}
alt="빼기"
onMouseDown={onDecrease}
onMouseUp={offPress}
></img>
</div>
);
};
useRef를 사용하고, setState의 비동기처리 특성도 살려서 일부 수정한 결과는 아래와 같습니다.
이제 마우스를 누르고 뗐을 때 원하는 대로 잘 동작하는 것을 확인할 수 있습니다.
const Stopwatch = () => {
const [time, setTime] = useState(0);
const isPressed = useRef(false);
const speed = useRef(300);
const onIncrease = () => {
isPressed.current = true;
repeatCount(1);
};
const offPress = () => {
isPressed.current = false;
speed.current = 300;
};
const onDecrease = () => {
isPressed.current = true;
repeatCount(-1);
};
const repeatCount = (cnt) => {
console.log("ww");
console.log("상태", isPressed, cnt, time);
if (isPressed.current) {
if (speed.current > 50) speed.current -= 10;
setTime((prev) => prev + cnt);
setTimeout(() => {
repeatCount(cnt);
console.log("w2w");
}, speed.current);
}
};
return (
<>
<div css={watchContainer}>
<img
src={Plus}
alt="더하기"
onMouseDown={onIncrease}
onMouseUp={offPress}
onMouseLeave={offPress}
></img>
<input type="text" value={time}></input>
<img
src={Minus}
alt="빼기"
onMouseDown={onDecrease}
onMouseUp={offPress}
onMouseLeave={offPress}
></img>
</div>
<div css={playContainer}>
<img src={Play} alt="시작"></img>
</div>
</>
);
};
2. 뽀나스(useRef에 DOM 담기)
이렇게 useRef를 리렌더링되지 않는 변수처럼 사용할 수도 있지만, 특정 DOM을 선택해야 할 때도 사용할 수 있습니다.
기존의 getElementById나 querySelector와 같은 DOM selector함수를 사용하는 것 대신 React에서는 useRef를 통해 Ref객체를 생성하고 이름을 붙여준 후에 원하는 DOM에 ref값으로 설정만 해주면 Ref객체의 current값은 선택한 DOM을 가리키게 됩니다.
아래는 특정 DOM을 선택하는 예시입니다.
inputRef는 return 안의 input태그로 시작하는 부분의 DOM을 담고 있게 되는 것이죠!
const test = () => {
const [inputs, setInputs] = useState("");
const inputRef = useRef();
return (
<div>
<input type="text" ref={inputRef} />
</div>
);
}
3. 참고자료
최근댓글