웹개발을 하다보면 자주 접하게 되는 UI 중 하나가 모달 혹은 팝업이다. 그런데 이런 모달이나 팝업을 띄웠을 때 고려해야 하는 것 중 하나가 바로 뒤로가기 버튼이다.
UX적으로 생각해보았을 때 모달은 페이지 변경이 아닌 레이어 위에 쌓이는 형태이기 때문에 라우팅 변경없이 띄워지고 사라질 때로 라우팅 변경없이 모달 창만 사라지는게 가장 자연스럽다고 생각했다.
또한, 만약 이 모달이 띄워진 상태에서 유저가 브라우저 창의 뒤로가기 버튼을 눌렀을 시에도 라우터가 뒤로가는 게 아닌 모달창이 닫히는 게 자연스러운 사용자 경험을 제공할 것이라 생각했다. 이러한 고민을 바탕으로 내가 어떤 식으로 모달을 관리했는지 공유해보려고 한다.
내가 생각한 로직은 다음과 같다.
- 모달을 열 때는 store에 현재 여는 모달을 기록하고 history를 한 번 쌓아준다.
- 모달을 store에 기록하는 이유는 브라우저 상에서 모달이나 팝업 등이 중첩으로 열리는 경우도 있기 때문에 이를 전역적으로 상태관리를 하기 위함이다.
- 브라우저 상에서 뒤로가기/닫기 액션이 이루어졌을 때는 store에 담아둔 모달 리스트를 체크하고 띄워져 있는 모달이 있다면 모달만 닫는다. 이 때 쌓아둔 히스토리를 제거한다.
// modal 관리를 위한 스토어 생성
export default {
name: 'modal',
state() {
return {
modalList: [] // 쌓인 모달을 담을 배열
};
},
getters: {
getModalList(state) {
return state.modalList;
}
},
mutations: {
// 모달이 띄워질 때마다 modalList에 추가
ADD_MODAL_LIST(state, value) {
state.modalList.push(value);
},
// 모달이 닫힐시 modalList에서 제거
REMOVE_MODAL_LIST(state, value) {
state.modalList.splice(value, 1);
},
// modalList 제일 뒤에 있는 요소 제거
POP_MODAL_LIST(state, value) {
state.modalList.pop();
}
},
actions: {
// 현재 닫으려고 하는 모달을 찾아 modalList에서 제거
// 다만 동일한 모달이 여러 개가 띄워졌을 경우 가장 위에 쌓인 하나만 제거 되도록 함
removeLastModal({ getters, commit }, modalElement) {
const modalList = window.$nuxt.$store.getters['router/getModalList'];
const findIndex = modalList.indexOf(modalElement);
if (findIndex > -1) {
window.$nuxt.$store.commit('router/REMOVE_MODAL_LIST', findIndex);
}
}
}
};
computed: {
...mapGetters({
modalList: 'modal/getModalList'
})
},
methods: {
...mapMutations({
ADD_MODAL_LIST: 'modal/ADD_MODAL_LIST',
POP_MODAL_LIST: 'modal/POP_MODAL_LIST'
}),
...mapActions({
removeLastModal: 'modal/removeLastModal'
})
...
}
// 모달, 팝업 띄우기
openModalHandler(component, props = {}, actions = {}) {
/* 컴포넌트 띄우는 함수(openHandler) 실행 시 mount와 close 이벤트 액션을 넘김 */
return this.openHandler(component, props, { ...actions,
// mount event action
mount: (componentEl) => {
// 컴포넌트가 마운트 되면 mount 이벤트를 emit
// history 쌓기
window.history.pushState(null, '', window.location.href);
this.ADD_MODAL_LIST(componentEl); // store에 마운트된 컴포넌트 담기
},
// close event action
close: (componentEl) => {
// 모달 내부적으로 존재하는 닫기 버튼을 누르면 close 이벤트 emit
// 컴포넌트 제거 함수 (closeHandler) 실행하여 띄워진 요소 닫기
this.closeHandler(componentEl);
this.removeLastModal(componentEl); // store removeLastModal 함수 실행
}
});
}
// 브라우저 뒤로가기 시 모달, 팝업만 닫기
closeModalHandler() {
// 가장 위에 쌓인 모달 요소 찾기
const modalEl = this.modalList[this.modalList.length - 1];
// 찾은 모달 닫기 액션
this.closeHandler(modalEl);
this.POP_MODAL_LIST(); // store에서 modal 제거
}
브라우저 페이지 이동 시에 발생하는 onpopstate
이벤트를 활용하여 브라우저 뒤로가기 이벤트를 감지시 예외처리를 해주었다.
// 브라우저 레이어가 mount 됐을 때 popstate 이벤트 핸들러 등록
mounted() {
window.addEventListener('popstate', this.historyHandler);
},
beforeDestroy() {
window.removeEventListener('popstate', this.historyHandler);
},
...
// 브라우저 뒤로가기 클릭시 모달만 닫히도록 예외처리
historyHandler() {
// 스토어 modalList length를 체크하여
// 떠 있는 팝업이 존재하면 팝업만 닫히게 closeModalHandler 실행
if (this.modalList.length > 0) {
this.closeModalHandler();
}
}