이상형 월드컵을 만들고 있다.
상태 업데이트가 필요한 상황이 많아서, 로직을 짜기가 까다로웠다.
이상형월드컵에서는 1:1 승리한 상대를 업데이트를 해야하는데,
const randomContestant = () => {
const randomIndex1 = Math.floor(Math.random() * contestants.length);
const randomIndex2 = randomIndex(randomIndex1);
const randomContestant1 = contestants[randomIndex1];
const randomContestant2 = contestants[randomIndex2];
setTwoPeople([randomContestant1, randomContestant2]);
setContestants((el: Contestant[]) => { // 핵심 로직
return el.filter(
(el) => el !== randomContestant1 && el !== randomContestant2
);
});
};
위 로직을 사용하여서 랜덤으로 2명을 뽑아오고,
const handleClick = (num: number) => {
setIsCheck([false, num]);
setWinner((prev) => [...prev, twoPeople[num]]);
//여기서 업데이트했을때 속도를 로직을 확인해야함
setTimeout(() => {
setIsCheck([true, 3]); //원위치
randomContestant(); //다시뽑기
}, 2200);
};
위 로직을 통해 모션 후 받아오는 방식을 취했다.
winner라는 곳에 백업을 해두고,
contestants가 0이 되면 넣어서 업데이트 하는 방식을 구상했다.
그런데, 0이 되는 순간, 넣으면 업데이트가 되지 않은 상태에서 로직이 돌아가 문제가 발생했고,
2차적인 방법으로
length가 2일때 미리 넣어주는 방법으로 시도하였다.
그랬더니 마지막으로 승리한 사람이 못들어왔다.
useState는 업데이트할 대상을 모은 다음 한번에 업데이트하기 때문에,
이러한 상황이 발생하였다.
처음에는 state를 바로 업데이트해주는 flushSync를 사용하는 방법을 고려했지만,
(flushSync
https://kyounghwan01.github.io/blog/React/React18/flushsync/#react18-state-%E1%84%87%E1%85%A2%E1%84%8E%E1%85%B5%E1%86%BC)
이는 강제적인 특성이 있어 리스크가 있을 것을 우려하였고, 좀 더 이해도가 높은
Ref로 선회하였다.
Ref는 바로바로 값을 업데이트해주는 대신 렌더링을 하지 않는다는 단점이 있다.
이를 통해 화면에서 업데이트가 되지 않는 것이 문제였다.
다행히도, Ref와 state를 적절히 섞어서 사용하면, 이런 Ref 단점을 상쇄해줄 것이라고 예측했고,
이러한 방법으로 구상하였다.
const randomContestant = () => {
const randomIndex1 = Math.floor(Math.random() * matchRef.current.length);
const randomIndex2 = randomIndex(randomIndex1, matchRef.current.length);
const randomContestant1 = matchRef.current[randomIndex1];
const randomContestant2 = matchRef.current[randomIndex2];
setTwoPeople([randomContestant1, randomContestant2]);
matchRef.current = matchRef.current.filter(
(el) => el !== randomContestant1 && el !== randomContestant2
);
// setContestants((el: Contestant[]) => {
// return el.filter(
// (el) => el !== randomContestant1 && el !== randomContestant2
// );
// });
};
먼저 기존에 있던 setContestants 대신에 mathcRef를 사용하여 관리를 하였고, 이를 통해 빠른 업데이트가 되도록 하였다.
setTwopeople도 Ref를 고려해볼 수는 있겠지만, 이 업데이트 순서에 맞춰 로직을 짜서 수정하지 않았다.
const handleClick = (num: number) => {
setIsCheck([false, num]); //이펙트 주고 뽑힌 사람 알려줌
//다시 뽑기 진행해야하고,
// 원래 상태로 돌려놔야함
//이긴 사람들 관리해야함
setWinner((prev) => [...prev, twoPeople[num]]);
//여기서 업데이트했을때 속도를 로직을 확인해야함
winnerRef.current = [...winnerRef.current, twoPeople[num]];
//setWinner((prev) => [...prev, twoPeople[num]]);
if (matchRef.current.length === 0) {
matchRef.current = winnerRef.current;
winnerRef.current = [];
}
setTimeout(() => {
console.log(winnerRef.current, "리퍼");
setIsCheck([true, 3]); //원위치
randomContestant(); //다시뽑기
}, 2200);
// setTimeout(() => {
//setIsCheck([true, 3]); //원위치
//randomContestant(); //다시뽑기
};
위와 같이 수정하였다. Ref를 사용하지 않을때는 언제 winners를 넣어줘서 match를 업데이트를 해야할지 감이오지 않았다.
하지만 ref를 사용하면 바로 업데이트 되기때문에 고민없이 뒤에 넣을 수 있었다.
이를 통해 원하는 상태로 업데이트 할 수 있었다.
좀더 ref는 직관적이어서 편하다.
이 후에도 버튼을 연속으로 누르면 버그가 발생하여서 이를 방지하기위해 버튼 방지 버튼도 추가하여서 처리하였다.
const isButtonDisabledRef = useRef(false);//추가
const handleClick = (num: number) => {
if (isButtonDisabledRef.current) {//추가
return;
}
isButtonDisabledRef.current = true; // 버튼 비활성화 추가
setIsCheck([false, num]); //이펙트 주고 뽑힌 사람 알려줌
//다시 뽑기 진행해야하고,
// 원래 상태로 돌려놔야함
//이긴 사람들 관리해야함
setWinner((prev) => [...prev, twoPeople[num]]);
//여기서 업데이트했을때 속도를 로직을 확인해야함
winnerRef.current = [...winnerRef.current, twoPeople[num]];
if (matchRef.current.length === 0) {
matchRef.current = winnerRef.current;
winnerRef.current = [];
}
setTimeout(() => {
console.log(winnerRef.current, matchRef.current, "타이머");
setIsCheck([true, 3]); //원위치
randomContestant(); //다시뽑기
isButtonDisabledRef.current = false; // 버튼 활성화 추가
}, 2200);
};
위와 같이 버튼 클릭시 이벤트 발생 후 사용할 수 있게 수정하였다.
useState는 플로우의 순서를 쫓으며 고려해야할 요소가 있는데, useRef는 확실히 직관적이어서 고민이 적어진다.
단점을 적절히 보완하고 사용한다면,
useState와 useRef를 유용하게 쓸 수 있겠다.
useRef 참 처음엔 진짜 어렵고 뭔가 쓰기 꺼렸는데 쓰면 쓸 수록 이해도 되고, 진짜 좋은 거 같아요. 잘보고 갑니다
+ 여담으로 저 뉴진스 토끼 개발자 버전 진짜 많이 보이던데 막 리액트 토끼도 있고