해가 쨍쨍한 어느 날, 어김없이 모닝 커피와 함께 하루를 시작했다.
갑자기 회의가 잡히더니 기존에 운영중이였던 서비스 중 하나를 재개발을 하는게 낫겠다 싶을 정도의 요구 사항이 들어왔다...
어쩐지 그 날따라 커피가 쓰더라...
데이터 구조 자체를 바꿔야하는 요구사항도 있어서 백엔드도 막막한 상황이였다.
그래서 🌟 재개발 🌟을 그냥 진행하기로 했다!
어차피 기존 서비스가 Vue.js
로 만들어져 있었고, 심지어 업데이트가 중지된 버전 2로 되어있었다. 😅
웹 브라우저 팝업과 데이터 동기화 라는 제목은 기존 서비스를 재개발 하면서 겪은 수많은 이슈들 중 하나이다.
재개발을 하면서 기존의 기능 + 추가 요구사항 때문에
메인으로 제일 많이 쓰이는 페이지는 기능이 굉장히 많았다.
💡 Soket 포함 32개의 API가 한 페이지에서 쓰였다. (직접 세어봤습니다)
디자이너가 다른 프로젝트 때문에 바빠서 직접 와이어 프레임을 잡아가며 혼자 작업을 하게 되었는데, 한 페이지에 기능이 너무 많아 고민을 많이 했었고 후에 서비스가 좀 더 확장되었을 때도 고려를 해야했다.
일단 제목에서 알 수 있듯 해당 페이지에서는 새 브라우저 팝업을 띄워서 작업을 해야했고, 새로 띄운 브라우저와 메인 브라우저의 데이터가 동기화 되어야 했다.
처음에는 어렵게 생각하지 않고 이렇게 생각했었던 것 같다.
"그냥 전역 상태 관리하는 라이브러리 사용하면 되겠다"
전역 상태 관리 라이브러리는 zustand
를 사용했는데, 새 브라우저 팝업을 열어서 사용하니 전역 상태 데이터들이 전혀 공유가 되지 않았다.
전역 상태 관리 라이브러리로의 일반적인 사용 방법으로는 사용이 불가 해서 zustand
의 persist
를 사용해서 localStorage
에 데이터를 저장해서 데이터를 공유했다.
근데 localStorage
는 데이터가 변경 되는게 확인이 되는데, zustand
의 기능으로는 각 브라우저가 전역 상태를 변경 했을 때의 이벤트를 감지하지 못했다.
상태가 변하는 이벤트를 감지 못하니 당연하게도 데이터를 서로 동기화하지 못하는 상황이 발생했다.
하지만 localStorage
는 변경이 되고 있기 때문에 직접 이벤트 리스너를 만들었다.
const useSyncWithPersistStore = (key: String) => {
const updateStore = () => {
useQaWorkStore.persist.rehydrate();
};
useEffect(() => {
const updateQaWorkStore = (event: StorageEvent) => {
if (event.key === key) updateStore();
};
window.addEventListener('storage', updateQaWorkStore);
return () => {
window.removeEventListener('storage', updateQaWorkStore);
};
}, []);
};
커스텀 훅으로 만든 뒤, 메인 브라우저의 해당 페이지와 팝업 브라우저에 각 한 개씩 선언해줬다.
하지만 위의 커스텀 훅을 사용할 때 문제점이 두 가지 있었다.
첫 번째 문제는 위 커스텀 훅의 목적은 포커싱이 되어있는 브라우저에서 데이터를 변경할 때, 포커싱이 되어 있지 않은 브라우저가 변경 이벤트를 감지한 후 데이터를 동기화 시켜줄 수 있도록 만들어 주는 것인데, 포커싱이 되어 있는 곳이 간헐적으로 커스텀 훅의 이벤트가 적용될 때가 있었다.
두 번째 문제는 포커싱된 브라우저에서 데이터를 변경하는 이벤트를 빠르게 여러 번 일으키게 되면, 버벅거림 현상이 자주 일어나 UX 적으로 매우 좋지 않았다.
첫 번째 문제는 커스텀 훅 안의 이벤트 리스너가 작동할 때 실행되는 함수가 실행되서 rehydrate
처리될 때, document.hasFocus() 메서드를 활용해서 사용자가 동작하고 있지 않은 브라우저에서만 함수가 작동하도록 제한을 두었다.
두 번째 문제는 Debounce
처리로 해결을 했다.
Debounce
란 특정 이벤트가 연속적으로 발생할 때, 마지막 이벤트가 발생하고 일정 시간 동안 더 이상 동일한 이벤트가 발생하지 않을 때까지 해당 이벤트 처리 함수를 지연시키는 방법이다.
Debounce
이벤트는 검색할 때의 keyboard 이벤트나 스크롤 이벤트 또는 브라우저 창 사이즈를 변경하는 이벤트에 주로 사용하는데, 나는 팝업 브라우저와 메인 브라우저 사이의 데이터 동기화 과정에서 발생하는 UX 문제의 해결 방법으로 활용을 했다.
아래는 해결한 이후의 코드이다.
const useSyncWithPersistStore = (key: string, time?: number) => {
let debounceTimer: NodeJS.Timeout | null = null;
const updateStore = () => {
if (!document.hasFocus()) useQaWorkStore.persist.rehydrate();
};
useEffect(() => {
const updateQaWorkStore = (event: StorageEvent) => {
if (event.key === key) {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
updateStore();
}, time || 250);
}
};
window.addEventListener('storage', updateQaWorkStore);
return () => {
window.removeEventListener('storage', updateQaWorkStore);
if (debounceTimer) clearTimeout(debounceTimer);
};
}, []);
};
다른 의견이나 잘못된 부분이 있다면 댓글 부탁드립니다!