오늘은 프로젝트를 개발할 때 빠질 수 없는 Modal창 구현에 대해 말해보고자 하는데요. 단순히 Modal만 구현을 하는 것이 아니라 Debouncing을 활용해 Modal창 활성화 버튼을 계속 눌렀을 때 해당 창이 꺼지지 않고 지속적으로 남아 있을 수 있도록 만드는 법에 대해 알아보도록 하겠습니다.
먼저, Debouncing에 대해 궁금하신 분들은 제가 썼던 바로 전 글을 읽어주시면 감사하겠습니다.
이번 Modal창 만드는 작업을 할 때에는 Lodash 라이브러리를 사용하지 않고 clearTimeout()과 setTimeout()을 활용해 만들어보겠습니다.
기존 모달창은 다음과 같습니다.
import { ConfirmModal } from '../SomeFolder/SomeFile'
const ModalEample () => {
const [showLoginModal, setShowLoginModal] = useState(false);
const text = "모달창 띄우기";
const onClickModal = () => {
setShowLoginModal(true);
setTimeout(() => {
setShowLoginModal(false);
}, 2000);
};
return (
<>
<button onClick={onClickModal}> 모달 띄우기 <button/>
{showLoginModal && (<ConfirmModal text={text}/>)}
</>
);
}
다음과 같은 컴포넌트가 있다고 할 때, setTimeout()을 통해 버튼이 클릭되고 열린 후에 2000ms가 지나면 해당 ConfirmModal은 닫히게 됩니다. 하지만 어떤 사용자가 버튼을 계속 누르면 어떻게 될까요?
onClickModal()을 통해 ture값을 가지게 된 showLoginModal은 사용자가 아무리 많이 버튼을 누른다고 해도 한 번 열린 모달을 계속 떠있게 할 수 없고 2000ms가 지나면 닫히게 됩니다.
이런 현상은 사용자 입장에서 직관적으로 와닿지 못하는 상황이기에 사용자가 누르면 그때부터 다시 setTimeout을 설정해주는 편이 자연스럽습니다.
이럴 때 사용할 수 있는 것은 바로 저번에도 다루었었던 clearTimeout()과 setTimeout()을 할당할 수 있는 변수 timer를 선언해주는 것인데요.
이 과정까지 적용해보도록 하겠습니다.
import { ConfirmModal } from '../SomeFolder/SomeFile'
const ModalEample () => {
const [showLoginModal, setShowLoginModal] = useState(false);
const text = "모달창 띄우기";
let timer;
const onClickModal = () => {
setShowLoginModal(true);
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
setShowLoginModal(false);
}, 2000);
};
return (
<>
<button onClick={onClickModal}> 모달 띄우기 <button/>
{showLoginModal && (<ConfirmModal text={text}/>)}
</>
);
}
위처럼 clearTimeout()과 timer를 사용해서 Debouncing을 구현해줬는데요. 작동을 잘 할까요?
답은 작동하지 않는다. 입니다.
왜냐하면 해당 onClickModal() 함수가 작동하면서 setShowLoginModal의 값을 바꾸고 이는 다시 컴포넌트가 리렌더링되게 하기 때문입니다. 컴포넌트가 다시 렌더링되면서 기존에 남아있는 setTimeout()을 제어하지 못한 채 실행되어 지속적으로 클릭을 했음에도 불구하고 2초마다 모달이 꺼지게 되는 것입니다.
그렇다면 이를 막을 수 있는 방법은 무엇이 있을까요?
useCallback
useCallback()은 함수를 메모이제이션(memoization)하기 위해서 사용되는 hook 함수
위에 나온 useCallback을 활용해 해결할 수 있습니다.
const add = useCallback(() => x + y, [x, y]);
이처럼 사용하게 될 경우 컴포넌트가 새로 렌더링되게 되더라도 x와 y의 값이 바뀌지 않으면 기존 함수를 계속해서 반환합니다.
이 개념을 활용해 컴포넌트가 렌더링 되더라도 timer가 바뀌지 않으면 기존 함수를 계속 사용할 수 있도록 적용해보겠습니다.
import { useCallback } from 'react'
import { ConfirmModal } from '../SomeFolder/SomeFile'
const ModalEample () => {
const [showLoginModal, setShowLoginModal] = useState(false);
const text = "모달창 띄우기";
let timer;
const onClickModal = useCallback(() => {
setShowLoginModal(true);
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
setShowLoginModal(false);
}, 2000);
}, [timer]);
return (
<>
<button onClick={onClickModal}> 모달 띄우기 <button/>
{showLoginModal && (<ConfirmModal text={text}/>)}
</>
);
}
이와 같이 사용하게 되면 컴포넌트가 리렌더링 된다고 할지라도 사용자가 버튼을 지속적으로 눌렀을 때 제대로 Debouncing되게 할 수 있습니다.