뒤로가기시, 모달 띄우기

sumi-0011·2024년 2월 26일
0

⏰ 10MM 개발기

목록 보기
2/5
post-thumbnail

요구사항
뒤로가기 버튼 클릭, 또는 뒤로가기 동작 (브라우저 동작)이 일어나면, 정말 끝내시겠습니다?라는 모달이 뜨게 됩니다.

단순히 뒤로가기 버튼 클릭에만 이벤트를 주는 것으로는 이 요구사항을 충족하기 어려웠습니다.
가장 어려웠던 부분은 브라우저의 history stack을 고려하며 브라우저의 뒤로가기 동작을 커스텀 하는 것이였습니다.

브라우저의 뒤로가기 동작 커스텀

보통 브라우저의 뒤로가기 동작 커스텀을 검색해본다면, 아래와 같은 코드를 찾을 수 있습니다.

function useCustomBack(customBack: () => void) {
  const browserPreventEvent = (event: () => void) => {
    history.pushState(null, '', location.href);
    event();
  };

  useEffect(() => {
    history.pushState(null, '', location.href);
    window.addEventListener('popstate', () => {
      browserPreventEvent(customBack);
    });
    return () => {
      window.removeEventListener('popstate', () => {
        browserPreventEvent(customBack);
      });
    };
  }, []);
}

브라우저의 뒤로가기 동작이 일어날 때 history stack에서 하나가 빠지고 popstate라는 이벤트가 발생합니다, 이를 감지해 현재 주소를 다시 히스토리 스택에 추가해 실질적으로는 페이지 이동이 되지 않고, 뒤로가기 동작이 일어나지 않는 것 같은 효과를 낼 수 있습니다.

여기서 현재 주소를 히스토리 스택에 추가할 때, 원하는 커스텀 동작을 같이 실행해 뒤로가기 동작이 일어나면 모달이 뜨는 등의 기능을 구현할 수 있습니다.

그런데 단순히 뒤로가기 동작을 막고 모달을 띄우는 것이 아니라, 해당 모달에서 끝내기버튼을 누르면 페이지가 뒤로가는 기능을 구현해야했습니다.

문제의 코드

// 끝내기 버튼 눌렀을 때
const onExit = () => {
  router.replace(ROUTER.MISSION.DETAIL(missionId));
};

// 취소 버튼 눌렀을 떄
const onBackMidModalClose = () => {
	closeBackMidOutModal(); // 화면에서 모달 제거
	onNextStep(prevStep); // 이전 진행상황으로 돌아감
};

// custom back hooks
function useCustomBack(customBack: () => void) {
  const browserPreventEvent = (event: () => void) => {
    history.pushState(null, '', location.href);
    event();
  };

  useEffect(() => {
    history.pushState(null, '', location.href);
    window.addEventListener('popstate', () => {
      browserPreventEvent(customBack);
    });
    return () => {
      window.removeEventListener('popstate', () => {
        browserPreventEvent(customBack);
      });
    };
  }, []);
}

처음에 작성했던 코드는 위와 같았습니다.
문제가 없는 것 처럼 보이지만, 히스토리 스택이 잘못 쌓이는 문제가 존재했습니다.

이전이전 페이지를 A, 이전 페이지를 B, 현재 페이지를 C라고 가정한다면
A -> B -> C 페이지로 차례대로 접근 후,
C 페이지에서 뒤로가 B페이지로 가고, B 페이지에서 다시 뒤로가기를 한다면 A 페이지로 가는 것이 정상적인 동작입니다.

하지만 위의 코드에서는 A 페이지가 아닌 다시 C 페이지로 돌아가는 이상현상이 발생하였습니다.

의도한 동작
A -> B -> C -> B -> A

실제 발생한 동작
A -> B -> C -> B -> C

해당 문제는,
C페이지에서 B페이지로 뒤로가기를 통해 이동할 때 replace를 통해 히스토리 스택에서 C 페이지를 삭제하고 넘어갔다고 생각하였는데, 히스토리에서 없어지지 않아 발생한 문제였습니다.

그렇다면 왜 히스토리 스택에서 사라지지 않았을까?

기본적으로 hisory.replace (=== router.replace)를 이용하면, 이동을 하기 전에 쌓인 히스토리를 덮어 씌우기 때문에 C -> B로 이동할 떄 C의 히스토리가 사라지는게 맞습니다.

실제로도 replace를 통해 이동했기 때문에, 하나의 C 히스토리는 삭제되었습니다.
하지만, 뒤로가기 커스텀 동작을 통해 C의 히스토리 스택이 두개가 쌓여있는 상태였기 때문에 두개의 히스토리중 하나의 히스토리만 삭제되어 히스토리 스택에서 C가 사라지지 않는 것 처럼 보였던 것이였습니다.

동작 플로우를 살펴보자

어떻게 히스토리 스택이 쌓이고 없어지길래 이러한 상황이 연출될지 시뮬레이션 해본 결과, 이러한 결론이 나왔습니다.

밑의 전체 동작을 하는 동안 히스토리가 어떻게 쌓이는지 확인해보겠습니다.

전체 동작
A -> B -> C ->(뒤로가기) B ->(뒤로가기) A

초기 상태 (A 페이지)

History Stack : [A]

A -> B

History Stack : [A, B]

B -> C

History Stack : [A, B, C]

C 페이지에서..

C 페이지에서 뒤로가기 커스텀 동작을 통해 추가적으로 새로운 히스토리가 쌓입니다.
관련 코드

  useEffect(() => {
    history.pushState(null, '', location.href);
  }, []);

이로 인해 히스토리 스택에 C가 하나 더 추가됩니다.

History Stack : [A, B, C, C]

C 페이지에서 뒤로가기 시도

뒤로가기를 시도하면 popstate 이벤트가 발생하고, 히스토리 스택에서 하나가 빠집니다.
그리고, popstate에 걸어둔 event listener로 인해 browserPreventEvent 메소드가 실행되고 다시 C 페이지가 히스토리 스택에 추가됩니다.
그리고 browserPreventEvent메소드에서 event 메소드가 실행되어 `

관련 코드

// custom back hooks
function useCustomBack(customBack: () => void) {
  const browserPreventEvent = (event: () => void) => {
    history.pushState(null, '', location.href);
    event();
  };

  useEffect(() => {
    history.pushState(null, '', location.href); // 관련 x
    window.addEventListener('popstate', () => {
      browserPreventEvent(customBack);
    });
    return () => {
      window.removeEventListener('popstate', () => {
        browserPreventEvent(customBack);
      });
    };
  }, []);
}

History Stack : [A, B, C, C]

정말 끝내시겠습니다? 모달에서 끝내기 버튼 클릭 or 취소 버튼 클릭

// 취소 버튼 눌렀을 떄
const onBackMidModalClose = () => {
	closeBackMidOutModal(); // 화면에서 모달 제거
	onNextStep(prevStep); // 이전 진행상황으로 돌아감
};

취소 버튼을 클릭하면 모달이 닫히고, 현재 페이지에 그대로 머물게 됩니다.

History Stack : [A, B, C, C]

// 끝내기 버튼 눌렀을 때
const onExit = () => {
  router.replace(ROUTER.MISSION.DETAIL(missionId));
};

끝내기 버튼을 클릭하면 replace를 통해 B페이지로 돌아갑니다.

History Stack : [A, B, C, B]

이때 히스토리 스택에 위와 같이 C가 존재하게 됩니다.
이런 상황에서 B 페이지에서 뒤로가기를 하면 다시 C 페이지로 돌아가는 문제점이 생깁니다.

따라서, 이떄 히스토리 스택에 C 페이지가 없도록 처리해주어야 했습니다!

문제 해결

브라우저의 뒤로가기 동작 후, 바로 히스토리 스택에 제물 스택 (C)를 추가해주는 것이 문제였습니다.

  1. C 페이지에서 -> History Stack : [A, B, C, C]
  2. 브라우저의 뒤로가기 동작 -> History Stack : [A, B, C]
  3. 모달 오픈
    3-1. 끝내기 버튼 클릭 -> History Stack : [A, B]
    3-2. 취소 버튼 클릭 -> History Stack : [A, B, C, C]

위와같은 플로우로 히스토리 스택이 쌓이도록 수정해야 했습니다.

수정된 코드

// 끝내기 버튼 눌렀을 때
const onExit = () => {
  router.back();
};

// 취소 버튼 눌렀을 떄
const onBackMidModalClose = () => {
	closeBackMidOutModal(); // 화면에서 모달 제거
	history.pushState({}, '', location.href); // History Stack에 현재 페이지 추가
	onNextStep(prevStep);

};

// custom back hooks
function useCustomBack(customBack: () => void) {
  const browserPreventEvent = (event: () => void) => {
    // history.pushState(null, '', location.href); // 제거, Modal에서 처리
    event();
  };

  useEffect(() => {
    history.pushState(null, '', location.href);
    window.addEventListener('popstate', () => {
      browserPreventEvent(customBack);
    });
    return () => {
      window.removeEventListener('popstate', () => {
        browserPreventEvent(customBack);
      });
    };
  }, []);
}

따라서 코드를 아래와 같이 수정하여 해결할 수 있었습니다 :D

profile
안녕하세요 😚

0개의 댓글