Angular

Angular - 뒤로 가기 방지 관련 이슈 및 해결 방법 (history)

인어공쭈 2024. 11. 1. 20:52

문제 개요

기존 코드에서 발생한 문제는 다음과 같다:

  1. 모달이 열려 있을 때 뒤로 가기 버튼을 누르면 페이지가 이동되는 문제
  2. 사용자가 뒤로 가기 버튼을 누르면 모달이 닫히기만 하고 페이지가 이동되지 않도록 해야 한다.
  3. Angular ngOnDestroy가 호출되지 않음
  4. 모달을 단순히 CSS로 보이고 숨기는 방식으로 구현해 Angular 컴포넌트가 DOM에서 완전히 제거되지 않기 때문에, 리소스 해제를 위한 ngOnDestroy 호출이 이루어지지 않음.

해결 접근 방식

  1. 모달 열림 시 history.pushState로 히스토리 추가
    • 모달이 열릴 때마다 history.pushState로 현재 페이지를 히스토리에 추가해 히스토리 스택을 쌓음. 이렇게 하면 뒤로 가기 버튼을 누를 때 페이지 이동 없이 popstate 이벤트가 발생함.
  2. popstate 이벤트를 이용해 뒤로 가기 버튼 동작을 모달 닫기로 변경
    • @HostListener 데코레이터로 popstate 이벤트를 감지하여, onPopState 메서드를 통해 뒤로 가기 버튼을 누를 때마다 모달만 닫고 history.pushState를 다시 호출해 페이지 이동을 막음.
  3. 모달 리소스 정리를 위한 onCloseModal 메서드 구현
    • ngOnDestroy가 호출되지 않는 상황에서 구독 해제와 리소스 정리를 위해 onCloseModal 메서드를 작성. 모달을 닫을 때마다 onCloseModal을 호출해 안전하게 리소스를 해제하도록 함.

 

코드 내용 (기존 코드는 생략)

@Component({
  selector: 'app-layout-menu',
  templateUrl: './menu.component.html',
})
export class LayoutMenuComponent implements AfterContentInit {
  
  // 뒤로 가기 감지하여 모달을 닫고 정리
  @HostListener('window:popstate', ['$event'])
  onPopState(event: PopStateEvent) {
    if (this.visible) {
      this.onCloseModal();
    }
  }
  
  // 모달을 닫고 수동으로 리소스를 정리하는 메서드
  onCloseModal() {
    this.$unsubscribe.next(); // 구독 해제
    this.$unsubscribe.complete();
    this.onClose();
  }
  
  private _initSubscription() {
    this.openMenuYn$.pipe(takeUntil(this.$unsubscribe)).subscribe((yn) => {
	    this.visible = yn;
	  });
  }
}

 

하지만 에러 발생!! 다시 디버깅!!

$unsubscribe를 통한 해제는 모달을 열고 닫을때 구독을 해제 시키기 때문에 감지가 안되었다. 그래서 구독을 계혹 유지 시켜주는게 필요한 것 같다.

@Component({
  selector: 'app-layout-menu',
  templateUrl: './menu.component.html',
})
export class LayoutMenuComponent implements AfterContentInit {
  
  // 뒤로 가기 감지하여 모달을 닫고 정리
  @HostListener('window:popstate', ['$event'])
  onPopState(event: PopStateEvent) {
    if (this.visible) {
      this.onClose();
      history.pushState(null, '', location.href); 
      event.preventDefault(); 
    }
  }
  
  onClose() {
    this._store.dispatch(ThisActions.closeMenu());
  }
  
  private _initSubscription() {
     this.openMenuYn$.pipe(takeUntil(this.$unsubscribe)).subscribe((yn) => {
      this.visible = yn;
    });
  }
}

설명

  1. $unsubscribe 제거: 구독을 해제하지 않고, openMenuYn$의 상태 변경을 계속 감지하여 모달이 다시 열리거나 닫힐 때 visible 상태가 반영되도록 수정
  2. popstate에서 모달만 닫기: 뒤로 가기 버튼을 누르면 popstate 이벤트에서 onClose을 호출하여 모달만 닫고, history.pushState를 다시 호출해 뒤로 가기 동작을 막는다.

참고

https://getthismoment.tistory.com/29

 

javaScript - 레이어 팝업 history.back 감지

웹 모바일 개발하면서 레이어팝업에서 뒤로가기는 누르게 되면 비정상적인 움직임을 하는 것 알게되었습니다. 레이어팝업에서 뒤로가기를 하게 되면 부모화면이 닫히거나 history.back()이 됩니다

getthismoment.tistory.com

 

하지만 또 이슈 발생 될때가 있고 안될때가 있다!!

히스토리가 없을땐 잘되는데 히스토리가 있는 경우엔 history.pushState(null, '', location.href)에서 이전 url 을 가지고 있어서 뒤로가기가 여전하게 작동함

개선된 구현 방안

모달이 열리고 닫힐 때마다 히스토리 스택을 조작하여, 사용자가 뒤로 가기를 누를 때 페이지 이동을 막고 모달 상태만 변경할 수 있다.

  1. 모달이 열릴 때 pushState를 호출하여 히스토리에 새로운 상태를 추가.
  2. popstate 이벤트에서 history.pushState를 한 번 더 호출하여 현재 URL을 다시 추가함으로써 뒤로 가기를 차단.
  3. 루트에서 컨트롤 하기 위해서 서비스로 모달 상태를 관리해서 구독하여 변경 사항을 감지한다.
//service 

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class ModalService {
  private modalVisibleSubject = new BehaviorSubject<boolean>(false);
  modalVisible$ = this.modalVisibleSubject.asObservable();

  openModal() {
    this.modalVisibleSubject.next(true);
    history.pushState(null, '', location.href); // 모달 열릴 때 히스토리 추가
  }

  closeModal() {
    this.modalVisibleSubject.next(false);
  }
}


//app.component

export class AppComponent implements OnInit {
  isModalVisible: boolean = false;

  constructor(private modalService: ModalService) {}

  ngOnInit() {
    this.modalService.modalVisible$.subscribe((isVisible) => {
      this.isModalVisible = isVisible;
    });
  }

  @HostListener('window:popstate', ['$event'])
  onPopState(event: PopStateEvent) {
    if (this.isModalVisible) {
      // 모달이 열려 있을 때 뒤로 가기 막기
      history.pushState(null, '', location.href);
      event.preventDefault();
    }
  }
}

// modal

  if(this.visible) {
    this.modalService.openModal();
  }
    // 뒤로 가기 감지하여 모달을 닫고 정리
  @HostListener('window:popstate', ['$event'])
  onPopState(event: PopStateEvent) {
    if (this.visible) {
      this.onClose();
      this.modalService.closeModal();
    }
  }

 

설명

  1. BehaviorSubject를 사용한 모달 상태 관리
  2. history.pushState 를 사용해서 히스토리 업데이트
  3. popstate 를 사용해서 뒤로가기 감지후 이후 액션 추가

하지만 이렇게 하니깐 또 뒤로가기 히스토리가 리셋되서 뒤로가기 버튼이 비활성화됌! 따라서 사용자에게 불편을 줄수 있기 때문에 이슈를 보류하는게 좋을거 같다.

 

이렇게 문제 해결을 하면서 알게 된 개념을 간단히 정리해보자

 

1. history API 기본 개념

 

웹 브라우저의 history API는 사용자가 이전 페이지로 돌아가거나 앞으로 이동할 수 있도록 히스토리 상태를 관리하는 기능을 제공한다. Angular와 같은 싱글 페이지 애플리케이션에서는 페이지 전체를 리로드하지 않고 URL 변경이나 상태 관리를 통해 새로운 상태를 히스토리에 추가할 수 있다. history API는 주로 다음 세 가지 메서드를 사용한다.

  • history.pushState(state, title, url): 현재 URL과 별개로 히스토리에 새로운 상태를 추가한다. 이 메서드는 히스토리에 새로운 상태를 쌓기 때문에, 뒤로 가기 버튼을 누르면 방금 추가한 상태로 돌아간다.
  • history.replaceState(state, title, url): 현재 히스토리 상태를 변경하지만, 새로운 상태를 쌓지는 않는다. URL은 변경되지만 뒤로 가기를 눌러도 이전 상태로 이동하지 않는다.
  • history.back(): 히스토리에서 뒤로 이동한다. 뒤로 가기 버튼을 누른 것과 동일한 효과를 낸다.

정리해보면:

  • replaceState 방식:
    • 모달이 열릴 때마다 히스토리에 새로운 상태가 추가되지 않음.
    • 뒤로 가기 버튼을 무력화하기 위해 모달이 열려 있을 때 popstate 이벤트에서 replaceState로 URL을 고정.
    • 적합한 상황: 모달을 닫을 때 뒤로 가기 히스토리에 영향을 주지 않고, 단순히 뒤로 가기를 무력화하고 싶을 때 적합.
  • pushState + back 방식:
    • 모달을 열 때마다 임시 상태를 추가하여 뒤로 가기 시 모달이 닫히는 효과를 줄 수 있음.
    • 적합한 상황: 모달을 열고 닫을 때마다 뒤로 가기 버튼으로 모달을 닫는 동작을 기대할 때 적합.

 

2. BehaviorSubject 의 특징

  1. 초기값이 필요함: BehaviorSubject는 생성 시 초기값을 필수로 요구한다. 이 초기값은 BehaviorSubject가 구독되기 전에도 접근할 수 있다.
  2. 가장 최근 값 저장: 언제든지 가장 최신 값을 유지하고 있어 새로운 구독자가 구독을 시작할 때 즉시 이 값을 받을 수 있다.
  3. 값 업데이트: next() 메서드를 사용하여 값을 업데이트하며, next()가 호출되면 모든 구독자가 이 새로운 값을 받게 된다.
  4. 상태 관리에 유용: Angular와 같은 프레임워크에서 BehaviorSubject는 상태 관리나 데이터 공유에 매우 유용하다. 컴포넌트 간 데이터 흐름을 쉽게 처리할 수 있으며, 전역 상태 관리에도 적합하다.

BehaviorSubject vs. Subject

  • 초기값 필요 여부: BehaviorSubject는 초기값이 필수이지만, Subject는 초기값이 없다.
  • 가장 최근 값 저장: BehaviorSubject는 항상 최신 값을 저장하고 있으며, 새로운 구독자는 최신 값부터 수신한다. 반면 Subject는 구독 시점 이후에 발생하는 값만 수신한다.
  • 상태 관리에 적합: BehaviorSubject는 최신 상태를 유지하고 새로운 구독자에게 이를 전달하기 때문에, 상태 관리에서 자주 사용된다.

 

3. asObservable()

BehaviorSubject나 Subject 또는 다른 Observable을 외부에 노출할 때 내부 데이터를 보호하기 위해 사용하는 메서드이다.

  • 데이터 보호: 외부에서 직접 값을 변경하지 못하도록 막을 수 있다.
  • 일방향 데이터 흐름 유지: 외부에서는 구독만 가능하고 값 변경은 서비스 내부 메서드를 통해서만 가능하게 한다.
반응형

'Angular' 카테고리의 다른 글

Angular - ControlValueAccessor  (0) 2024.09.12
Angular - 동작원리 및 RxJS와 Ngrx  (0) 2024.08.22
Angular Google Maps (AGM) 사용법  (1) 2024.07.11
Angular 유닛 테스트: Jasmine vs Jest 비교  (0) 2023.05.23
Angular - RxJS 란?  (0) 2023.03.29