문제 개요
기존 코드에서 발생한 문제는 다음과 같다:
- 모달이 열려 있을 때 뒤로 가기 버튼을 누르면 페이지가 이동되는 문제
- 사용자가 뒤로 가기 버튼을 누르면 모달이 닫히기만 하고 페이지가 이동되지 않도록 해야 한다.
- Angular ngOnDestroy가 호출되지 않음
- 모달을 단순히 CSS로 보이고 숨기는 방식으로 구현해 Angular 컴포넌트가 DOM에서 완전히 제거되지 않기 때문에, 리소스 해제를 위한 ngOnDestroy 호출이 이루어지지 않음.
해결 접근 방식
- 모달 열림 시 history.pushState로 히스토리 추가
- 모달이 열릴 때마다 history.pushState로 현재 페이지를 히스토리에 추가해 히스토리 스택을 쌓음. 이렇게 하면 뒤로 가기 버튼을 누를 때 페이지 이동 없이 popstate 이벤트가 발생함.
- popstate 이벤트를 이용해 뒤로 가기 버튼 동작을 모달 닫기로 변경
- @HostListener 데코레이터로 popstate 이벤트를 감지하여, onPopState 메서드를 통해 뒤로 가기 버튼을 누를 때마다 모달만 닫고 history.pushState를 다시 호출해 페이지 이동을 막음.
- 모달 리소스 정리를 위한 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;
});
}
}
설명
- $unsubscribe 제거: 구독을 해제하지 않고, openMenuYn$의 상태 변경을 계속 감지하여 모달이 다시 열리거나 닫힐 때 visible 상태가 반영되도록 수정
- popstate에서 모달만 닫기: 뒤로 가기 버튼을 누르면 popstate 이벤트에서 onClose을 호출하여 모달만 닫고, history.pushState를 다시 호출해 뒤로 가기 동작을 막는다.
참고
https://getthismoment.tistory.com/29
하지만 또 이슈 발생 될때가 있고 안될때가 있다!!
히스토리가 없을땐 잘되는데 히스토리가 있는 경우엔 history.pushState(null, '', location.href)에서 이전 url 을 가지고 있어서 뒤로가기가 여전하게 작동함
개선된 구현 방안
모달이 열리고 닫힐 때마다 히스토리 스택을 조작하여, 사용자가 뒤로 가기를 누를 때 페이지 이동을 막고 모달 상태만 변경할 수 있다.
- 모달이 열릴 때 pushState를 호출하여 히스토리에 새로운 상태를 추가.
- popstate 이벤트에서 history.pushState를 한 번 더 호출하여 현재 URL을 다시 추가함으로써 뒤로 가기를 차단.
- 루트에서 컨트롤 하기 위해서 서비스로 모달 상태를 관리해서 구독하여 변경 사항을 감지한다.
//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();
}
}
설명
- BehaviorSubject를 사용한 모달 상태 관리
- history.pushState 를 사용해서 히스토리 업데이트
- 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 의 특징
- 초기값이 필요함: BehaviorSubject는 생성 시 초기값을 필수로 요구한다. 이 초기값은 BehaviorSubject가 구독되기 전에도 접근할 수 있다.
- 가장 최근 값 저장: 언제든지 가장 최신 값을 유지하고 있어 새로운 구독자가 구독을 시작할 때 즉시 이 값을 받을 수 있다.
- 값 업데이트: next() 메서드를 사용하여 값을 업데이트하며, next()가 호출되면 모든 구독자가 이 새로운 값을 받게 된다.
- 상태 관리에 유용: 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 |