INDEX
Stack
#ReactJs
, #useEffect
, #addEventListener
Preview
보통 리액트에서는 토글 기능이나 Toast
, window
와 같은 status가 'ON', 'OFF' 인 컴포넌트를 구현할 때 useState를 사용합니다. 만약 아래와 같은 상황에서 어떻게 구현해야 할까요?
위 이미지에서 왼쪽에 있는 버튼은 공지사항 내용이 있으면 빨간 동그라미가 있고 없으면 표시하지 않도록 구현을 하려 합니다.
노티 이미지를 클릭했을 때 위 이미지처럼 공지사항 내용이 보여져야(show) 하고 해당 공지사항 영역 외의 영역을 터치하거나 노티 이미지 영역을 다시 클릭하면 공지사항 영역이 다시 hide 되어야 합니다.
Methodology
처음엔 단순하게 생각했습니다.
"useState로 공지사항을 보여줄지 말지 결정하는 state 변수를 하나 만들고 공지사항 영역 밖을 클릭하면 이 state 를 false 로 만들어 렌더링하지 않으면 되는거 아냐?" 아래와 같이 구현해봤습니다.
const notiRef = useRef(null);
/* component 영역 */
<img className="w-8 h-8 mx-2 cursor-pointer" src={notification} alt="notification" ref={notiRef}></img>
useRef
react-document 를 사용해 Noti image 컴포넌트를 참조하는 변수 notiRef 를 선언합니다. 이젠 addEventListener
를 사용해 클릭 이벤트를 구현해줍니다.
Noti Component가 Render 될 때마다 addEventListener가 할당되면 안되니 useEffect
안에 넣어줍니다.
useEffect(() => {
document.addEventListener("mousedown", (e: any) => {
if (notiRef.current && notiRef.current !== e.target) {
setNotiShow(false);
} else if (notiRef.current && notiRef.current === e.target)
setNotiShow(!notiShow);
});
}, []);
addEventListener
안에는 Noti 버튼을 클릭했으면 Toggle
기능을, Noti 버튼을 제외한 영역을 클릭했으면 notiShow를 false로 바꾸어 보이지 않도록 처리를 해주었으나 Noti 버튼을 제외한 영역을 클릭했을 경우에만 원하던 대로 동작했습니다.
Noti버튼 영역을 두 번 클릭했을 때 log를 찍어보면 아래와 같습니다.
document.addEventListener("mousedown", (e: any) => {
console.log(notiShow);
console.log(notiRef.current === e.target);
if (notiRef.current && notiRef.current !== e.target) {
setNotiShow(false);
} else if (notiRef.current && notiRef.current === e.target)
setNotiShow(!notiShow);
});
당연히 처음엔 notiShow가 false이고 버튼 영역을 클릭했으니 notiRef.current===e.target 의 결과값은 true입니다. 여기까진 오케이!
그런데 왜 두번째에도 notiShow가 여전히 false일까...
분명 처음 클릭으로 notiShow는 true가 됐고 심지어 noti 내용도 보이고 있는 상황입니다.
관련 내용을 찾아보았습니다.
The value of "notiShow(각색)" inside the scope of your useEffect is never updated since the effect was fired only once at mount. The State changes you make afterwards will never change this value unless you fire the Effect again but passing an argument into the square brackets. -stackoverflow
useEffect의 dependencies에 빈 배열을 넣으면 맨 처음 Render
될 때의 state
를 계속 가지고 있겠다는 뜻이라고 합니다. 그렇다면 useState(false)
로 notiShow 초기값을 false로 만든 이후 addEventListener의 콜백함수로 notiShow 값을 변경한다고 해도 useEffect callback 함수 스코프 안에서의 notiShow는 여전히 false인 상태입니다.
Stale Variables
Stale variables is a phenomenon when using React hooks to develop applications that happens if a state variable or prop has been changed but an effect has not been updated with the new value and is still using an old value. This can happen because the state variable or prop has not been included in the dependancy array properly.
이런 현상을 Stale Variables라고 하는데 dependency 배열에 적절한 state 나 prop을 넣어주지 않아서 state 가 변경된 값이 아닌 이전(old) 값을 가지고 있어서 그렇다고 합니다.
그럼 dependency array에 어떻게 넣어야 하는가...
우선 useEffect를 분리해줍니다.
Noti 내용이 있는지 조회하는 API 호출 부분은 어떤 dependency 에 관계없이 딱 한 번만 호출되어야 하기 때문에
dependency arr가 빈 useEffect 안에 넣어둡니다.
useEffect(() => {
const getNoti = async () => {
let result = await fetchApi("getNoti", "GET");
if (result.success) {
setNoti([...result.notis]);
}
};
getNoti();
}, []);
useEffect는 여러 개를 사용해도 그 dependency arr에 따라 각각 동작하기 때문에 상관없습니다.
useEffect(() => {
const handleNoti = (e: any) => {
if (notiRef.current && notiRef.current !== e.target) {
setNotiShow(false);
} else if (notiRef.current && notiRef.current === e.target) {
setNotiShow(!notiShow);
}
};
document.addEventListener("mousedown", handleNoti);
return () => {
document.removeEventListener("mousedown", handleNoti); //Component unmount 처리
};
}, [notiShow]);
처음 코드와 다른점은 컴포넌트가 unmount
될 때 mousedown
이벤트를 제거해 줬다는 것과 dependency arr에 notiShow 값을 추가해 줬다는 것입니다. 만약 Component unmount
처리를 해주지 않았다면 무수히 중첩되는 event listener를 만들어 낼 것이고 마찬가지로 dependency arr가 빈 값이었다면 처음과 같이 stale variable 현상이 재현될 겁니다.
또 다른 방법은?
useRef Hook을 사용하여 notiShow 값을 저장하고 있는 Ref 객체를 만듭니다. Link
function Noti() {
const [noti, setNoti] = useState<Tnoti[]>([]);
const [notiShow, setNotiShow] = useState(false);
const notiRef = useRef(null);
const notiShowRef = useRef(notiShow);
useEffect(() => {
const getNoti = async () => {
let result = await fetchApi("getNoti", "GET");
if (result.success) {
setNoti([...result.notis]);
}
};
getNoti();
document.addEventListener("mousedown", (e: any) => {
console.log(notiShowRef.current);
console.log(notiRef.current === e.target);
if (notiRef.current && notiRef.current !== e.target) {
notiShowRef.current = false;
setNotiShow(false);
} else if (notiRef.current && notiRef.current === e.target) {
notiShowRef.current = !notiShowRef.current;
setNotiShow(notiShowRef.current);
}
});
}, []);
notiShowRef
값은 notiShow state를 참조하고 있는 객체입니다. notiShowRef를 console에 찍어보면 다음과 같습니다.
얼핏 보면 그냥 Object 아냐?라고 할 수 있겠지만 리액트에서는 다르게 활용됩니다.
useRef 함수는 current 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current 속성에 할당합니다. 이 current 속성은 값을 변경해도 상태를 변경할 때처럼 React 컴포넌트가 다시 랜더링 되지 않습니다. React 컴포넌트가 다시 랜더링 될 때도 마찬가지로 이 current 속성의 값이 유실
되지 않습니다. -
즉, 컴포넌트의 렌더에 영향을 받지 않는 객체를 생성할 수 있게 됩니다.
useRef가 아닌 일반 Object를 만들어두면 notiShow값이 변경될 때마다 컴포넌트가 re-render 되어 object 가 매번 생성이 되기 때문에 이 객체를 사용할 순 없겠죠. 사실 notiShowRef에 앞서 notiRef 도 useRef를 통해 만든 객첸데 이 notiRef는 초기값으로 null을 넣어지만 noti img 태그 안에 ref 값으로 할당해 주어 current 값을 noti img element로 변경해 주었습니다.
이 객체 역시 컴포넌트의 렌더에 영향을 받지 않고 img element가 current 값으로 들어가 있어 addEventListener 안에서 noti img를 클릭했냐 안 했냐를 구분하는 데 사용할 수 있습니다.
이렇게 useRef를 사용해 dependency array를 신경 쓰지 않고 해결할 수도 있지만 저는 앞선 방법을 선호합니다.
setNotiShow와 notiShowRef.current =[boolean] 이 두 코드가 동일한 동작을 하는 것처럼 느껴지기 때문입니다.
Reference
https://www.daleseo.com/react-hooks-use-ref/'Front-End' 카테고리의 다른 글
초성 검색 기능 구현하기 (0) | 2023.08.23 |
---|---|
프로그래머스 과제 테스트-좋아하는 언어 검색기 (0) | 2023.05.11 |
OpenWeatherMap api by fetch and axios (0) | 2023.05.10 |
Delaying of Function (0) | 2023.04.11 |
[JS] Object 는 call by reference (0) | 2023.01.16 |