Vue.js에서 페이지 이동과 브라우저 뒤로가기 제어하기
1. 들어가며
Vue.js 프로젝트에서 페이지 이동 기능을 구현하다 보면 단순히 “다른 페이지로 이동하는 것” 외에도 고민해야 하는 부분이 있습니다.
예를 들어 로그인 성공 후 홈 화면으로 이동했을 때, 사용자가 브라우저 뒤로가기를 눌러 다시 로그인 화면으로 돌아갈 수 있습니다. 또는 결제나 설문 제출이 완료된 뒤 이전 작성 화면으로 돌아가 다시 요청을 시도할 수도 있습니다.
이런 상황을 처리하기 위해 Vue Router의 push, replace와 브라우저 히스토리 API의 동작 방식을 이해할 필요가 있었습니다.
이번 글에서는 다음 내용을 정리합니다.
- 브라우저 히스토리란 무엇인지
- Vue Router의
push와replace차이 replace로 모든 히스토리를 삭제할 수 있는지- 브라우저 뒤로가기 방지 방식
- 공통 페이지 이동 함수
goPage()만들기
2. 왜 페이지 이동과 뒤로가기 제어가 필요했을까?
2.1 처음 마주한 문제
특정 작업을 완료한 뒤 사용자가 브라우저 뒤로가기를 누르면 이전 화면으로 돌아가는 문제가 있었습니다.
예를 들면 다음과 같은 상황입니다.
- 로그인 성공 후 뒤로가기를 누르면 로그인 화면으로 돌아가는 경우
- 회원가입 완료 후 뒤로가기를 누르면 회원가입 입력 화면으로 돌아가는 경우
- 결제 완료 후 뒤로가기를 누르면 결제 정보 입력 화면으로 돌아가는 경우
- 설문 또는 시험 제출 후 뒤로가기를 누르면 답안 작성 화면으로 돌아가는 경우
일반적인 페이지 이동에서는 뒤로가기가 자연스러운 기능이지만, 이미 완료된 작업의 이전 화면으로 돌아가는 것은 화면 상태가 꼬이거나 중복 요청으로 이어질 수 있습니다.
2.2 모든 화면에서 뒤로가기를 막아야 할까?
그렇지는 않습니다.
다음과 같은 일반적인 화면 이동에서는 사용자가 이전 화면으로 돌아갈 수 있어야 합니다.
- 목록 페이지 → 상세 페이지
- 검색 결과 → 상품 상세 페이지
- 상품 상세 → 리뷰 작성 페이지
- 게시글 목록 → 게시글 상세 페이지
반대로 다음과 같은 완료 화면에서는 이전 화면으로 돌아가지 못하게 하는 것을 고려할 수 있습니다.
- 로그인 완료 → 홈 화면
- 회원가입 완료 → 로그인 화면
- 결제 완료 → 결제 완료 화면
- 설문 제출 완료 → 결과 화면
즉, 화면의 성격에 따라 페이지 이동 방식을 다르게 선택해야 했습니다.
2.3 해결하고 싶었던 목표
페이지 이동 기능을 만들며 해결하고 싶었던 목표는 다음과 같습니다.
- 일반적인 페이지에서는 자연스럽게 뒤로가기가 가능해야 한다.
- 특정 완료 화면에서는 직전 화면으로 돌아가지 못하게 해야 한다.
- 매번
router.push()와router.replace()를 직접 작성하지 않고 공통 함수로 관리하고 싶다.
3. 브라우저 히스토리란?
브라우저는 사용자가 방문한 페이지를 히스토리 스택에 순서대로 저장합니다.
[A] → [B] → [C]
↑ 현재 페이지
사용자가 뒤로가기를 누르면 현재 위치가 이전 히스토리로 이동합니다.
[A] → [B] → [C]
↑ 현재 페이지
Vue Router를 사용하는 SPA에서도 브라우저 히스토리를 기반으로 페이지 이동과 뒤로가기 기능이 동작합니다.
4. Vue Router의 페이지 이동 방식
Vue Router에서 페이지 이동을 할 때 주로 사용하는 메서드는 router.push()와 router.replace()입니다.
4.1 router.push()
router.push()는 새로운 페이지를 히스토리에 추가합니다.
router.push('/home')
히스토리는 다음과 같이 변경됩니다.
이동 전
[A, 로그인]
↑
router.push('/home')
이동 후
[A, 로그인, 홈]
↑
따라서 사용자가 뒤로가기를 누르면 로그인 화면으로 돌아갈 수 있습니다.
4.2 router.replace()
router.replace()는 새로운 히스토리를 추가하지 않고, 현재 히스토리를 새 페이지로 교체합니다.
router.replace('/home')
히스토리는 다음과 같이 변경됩니다.
이동 전
[A, 로그인]
↑
router.replace('/home')
이동 후
[A, 홈]
↑
로그인 화면이 홈 화면으로 교체되었기 때문에, 사용자가 뒤로가기를 눌러도 로그인 화면으로는 돌아갈 수 없습니다.
4.3 push와 replace 비교
| 구분 | router.push() | router.replace() |
|---|---|---|
| 히스토리 추가 | O | X |
| 현재 히스토리 교체 | X | O |
| 직전 화면으로 뒤로가기 | 가능 | 불가능 |
| 사용 예시 | 목록 → 상세 | 로그인 → 홈 |
5. replace는 모든 히스토리를 삭제할까?
처음에는 특정 페이지로 이동하면서 이전 히스토리를 모두 제거할 수 있을 것이라고 생각했습니다.
하지만 replace는 브라우저 히스토리 전체를 삭제하는 기능이 아닙니다.
replace는 현재 위치의 히스토리 한 칸만 교체합니다.
이동 전
[A, B, 로그인]
↑
router.replace('/home')
이동 후
[A, B, 홈]
↑
위 예시에서 로그인 화면은 홈 화면으로 교체되었지만, A, B 페이지는 그대로 남아 있습니다.
즉, 사용자가 뒤로가기를 누르면 B 페이지로 이동할 수 있습니다.
브라우저는 보안상 웹사이트가 사용자의 전체 방문 기록을 마음대로 삭제하지 못하도록 제한하고 있습니다.
그래서 아래와 같은 API는 존재하지 않습니다.
window.history.clear()
window.history.deleteAll()
이미 쌓여 있는 브라우저 히스토리를 모두 삭제하는 것은 불가능합니다.
6. 브라우저 뒤로가기 방지 방식
replace만으로 부족하고, 뒤로가기 버튼 자체를 눌러도 현재 화면을 유지해야 하는 특수한 상황에서는 브라우저 히스토리 API를 활용할 수 있습니다.
이때 사용하는 것이 window.history.pushState()입니다.
6.1 window.history.pushState()란?
pushState()는 현재 URL을 유지한 상태로 브라우저 히스토리에 새로운 항목을 추가하는 API입니다.
window.history.pushState(
{ preventBack: true },
'',
window.location.href
)
위 코드는 현재 URL을 그대로 히스토리에 한 번 더 쌓습니다.
호출 전
[A, 현재 페이지]
↑
pushState 호출 후
[A, 현재 페이지, 현재 페이지]
↑
6.2 null과 { preventBack: true }의 차이
다음 두 코드는 모두 히스토리를 쌓는다는 점에서는 동일합니다.
window.history.pushState(null, '', window.location.href)
window.history.pushState(
{ preventBack: true },
'',
window.location.href
)
차이는 첫 번째 인자인 state입니다.
{ preventBack: true }
이 값은 해당 히스토리가 뒤로가기 방지를 위해 만든 더미 히스토리라는 표시 역할을 합니다.
나중에 popstate 이벤트에서 다음처럼 확인할 수 있습니다.
window.addEventListener('popstate', (event) => {
console.log(event.state)
})
다만 단순히 히스토리를 쌓기만 하는 경우에는 null을 사용해도 동작에는 큰 차이가 없습니다.
6.3 더미 히스토리만 한 번 추가하면 생기는 문제
다음 코드처럼 pushState()를 한 번만 호출하면 어떻게 될까요?
window.history.pushState(
{ preventBack: true },
'',
window.location.href
)
처음 한 번 뒤로가기를 누르면 같은 URL의 더미 히스토리로 이동하기 때문에 화면이 그대로인 것처럼 보일 수 있습니다.
하지만 두 번째 뒤로가기를 누르면 더 이전 페이지로 이동할 수 있습니다.
[A, 현재 페이지, 현재 페이지(더미)]
↑
뒤로가기 1회
[A, 현재 페이지, 현재 페이지(더미)]
↑
뒤로가기 2회
[A, 현재 페이지, 현재 페이지(더미)]
↑
따라서 계속해서 뒤로가기를 방지하려면, 뒤로가기 이벤트가 발생할 때마다 다시 더미 히스토리를 추가해야 합니다.
7. popstate 이벤트로 뒤로가기 감지하기
브라우저에서 뒤로가기 또는 앞으로가기를 실행하면 popstate 이벤트가 발생합니다.
window.addEventListener('popstate', () => {
console.log('뒤로가기 또는 앞으로가기 발생')
})
이 이벤트를 활용하면 사용자가 뒤로가기를 눌렀을 때 다시 현재 페이지를 히스토리에 쌓을 수 있습니다.
window.addEventListener('popstate', () => {
window.history.pushState(
{ preventBack: true },
'',
window.location.href
)
})
동작 흐름은 다음과 같습니다.
1. 현재 페이지에 더미 히스토리를 추가한다.
2. 사용자가 뒤로가기를 누른다.
3. popstate 이벤트가 발생한다.
4. 이벤트 핸들러에서 다시 더미 히스토리를 추가한다.
5. 결과적으로 현재 페이지를 유지한다.
8. preventBrowserBack() 만들기
뒤로가기 방지 기능을 공통 함수로 만들 수 있습니다.
let backNavigationHandler = null
/**
* 브라우저 뒤로가기 방지
*/
export function preventBrowserBack() {
// 서버 렌더링 환경에서는 window 객체가 없으므로 종료
if (typeof window === 'undefined') return
// 기존 이벤트 리스너가 있다면 제거
if (backNavigationHandler) {
window.removeEventListener('popstate', backNavigationHandler)
}
// 현재 URL을 더미 히스토리로 추가
window.history.pushState(
{ preventBack: true },
'',
window.location.href
)
// 뒤로가기 발생 시 다시 더미 히스토리 추가
backNavigationHandler = () => {
window.history.pushState(
{ preventBack: true },
'',
window.location.href
)
}
// 뒤로가기 이벤트 등록
window.addEventListener('popstate', backNavigationHandler)
}
8.1 서버 환경 체크가 필요한 이유
Nuxt처럼 SSR(Server Side Rendering)을 사용하는 프로젝트에서는 코드가 서버에서도 실행될 수 있습니다.
서버에는 window 객체가 없기 때문에 아래와 같은 에러가 발생할 수 있습니다.
window is not defined
그래서 다음 코드를 추가합니다.
if (typeof window === 'undefined') return
일반적인 Vue SPA에서도 넣어두면 안전합니다.
8.2 중복 이벤트 리스너를 방지해야 하는 이유
preventBrowserBack()을 여러 번 호출하면 popstate 이벤트 리스너도 계속 추가될 수 있습니다.
preventBrowserBack()
preventBrowserBack()
preventBrowserBack()
이렇게 되면 뒤로가기 한 번에 이벤트 핸들러가 여러 번 실행될 수 있습니다.
그래서 기존 핸들러를 저장해두고, 새로 등록하기 전에 제거하는 방식으로 관리합니다.
if (backNavigationHandler) {
window.removeEventListener('popstate', backNavigationHandler)
}
8.3 Vue 컴포넌트에서 사용하는 방법
예를 들어 결제 완료 페이지에서 뒤로가기를 막고 싶다면 다음처럼 사용할 수 있습니다.
<script setup>
import { onMounted } from 'vue'
import { preventBrowserBack } from '@/utils/navigation'
onMounted(() => {
preventBrowserBack()
})
</script>
다만 이 기능은 사용자 경험을 해칠 수 있으므로, 일반 페이지가 아닌 꼭 필요한 화면에서만 사용해야 합니다.
9. preventBrowserBack() 사용 시 주의사항
preventBrowserBack()은 뒤로가기 버튼을 완벽하게 사전에 차단하는 기능은 아닙니다.
사용자가 뒤로가기를 누르면 브라우저는 실제로 이전 히스토리로 이동하려고 시도합니다. 이후 popstate 이벤트가 발생하고, 코드가 다시 현재 페이지 히스토리를 추가하는 방식입니다.
즉, “뒤로가기 자체를 막는 것”이라기보다 “뒤로가기가 발생한 뒤 현재 페이지 상태를 다시 유지하는 방식”에 가깝습니다.
또한 다음과 같은 이유로 남용하지 않는 것이 좋습니다.
- 사용자가 브라우저의 기본 동작을 사용할 수 없게 됨
- 모바일 브라우저의 스와이프 뒤로가기와 충돌할 수 있음
- 사용자가 페이지에서 빠져나오기 어려워질 수 있음
- 접근성과 사용자 경험이 나빠질 수 있음
따라서 우선은 router.replace()로 해결하고, 정말 뒤로가기가 위험한 상황에서만 preventBrowserBack()을 고려하는 것이 좋습니다.
10. goPage() 공통 페이지 이동 함수 만들기
페이지마다 아래 코드를 반복해서 작성할 수 있습니다.
router.push('/product/1')
router.replace('/home')
하지만 페이지가 많아질수록 어떤 화면에서 push를 사용하고 어떤 화면에서 replace를 사용하는지 관리하기 어려워집니다.
그래서 공통 페이지 이동 함수인 goPage()를 만들 수 있습니다.
10.1 canGoBack 옵션 설계
canGoBack이라는 이름으로 “이동 후 사용자가 이전 페이지로 돌아갈 수 있는지”를 표현할 수 있습니다.
canGoBack: true→router.push()canGoBack: false→router.replace()
10.2 goPage() 구현
import { useRouter } from 'vue-router'
export function useGoPage() {
const router = useRouter()
/**
* 공통 페이지 이동 함수
*
* @param {Object} options
* @param {string} options.path 이동할 경로
* @param {boolean} options.canGoBack 뒤로가기 가능 여부
*/
function goPage({ path, canGoBack = true }) {
if (canGoBack) {
return router.push(path)
}
return router.replace(path)
}
return { goPage }
}
10.3 사용 예시
일반적인 페이지 이동에서는 뒤로가기가 가능해야 하므로 canGoBack: true를 사용합니다.
goPage({
path: '/product/1',
canGoBack: true,
})
로그인 성공 후 홈으로 이동하는 경우에는 로그인 페이지로 돌아가지 못하도록 canGoBack: false를 사용할 수 있습니다.
goPage({
path: '/home',
canGoBack: false,
})
기본값이 true이므로 일반 이동에서는 경로만 전달해도 됩니다.
goPage({
path: '/product/1',
})
11. replace와 preventBrowserBack은 언제 함께 사용할까?
대부분의 경우에는 replace만 사용해도 충분합니다.
11.1 replace만 사용해도 되는 경우
다음과 같은 경우에는 현재 화면을 교체하는 것만으로 충분합니다.
- 로그인 완료 후 홈 화면 이동
- 회원가입 완료 후 로그인 화면 이동
- 온보딩 완료 후 메인 화면 이동
- 비밀번호 변경 완료 후 로그인 화면 이동
goPage({
path: '/home',
canGoBack: false,
})
11.2 preventBrowserBack까지 고려할 수 있는 경우
다음과 같이 이전 화면으로 돌아갔을 때 중복 요청, 중복 결제, 상태 불일치 등의 문제가 발생할 수 있다면 preventBrowserBack()을 추가로 검토할 수 있습니다.
- 결제 완료 후 결제 정보 입력 화면으로 돌아가는 경우
- 시험 제출 후 답안 작성 화면으로 돌아가는 경우
- 설문 제출 후 다시 제출을 시도할 수 있는 경우
- 민감한 인증 절차가 완료된 뒤 이전 인증 화면으로 돌아가는 경우
하지만 이 경우에도 프론트엔드에서만 막는 것으로 끝내면 안 됩니다.
예를 들어 중복 결제를 막아야 한다면 서버에서도 중복 결제 요청을 방지해야 합니다. 브라우저 뒤로가기 방지는 사용자 경험을 보완하는 역할이고, 실제 데이터 무결성은 서버에서 보장해야 합니다.
11.3 상황별 권장 방식
| 상황 | 권장 방식 |
|---|---|
| 목록 → 상세 | push |
| 검색 결과 → 상세 | push |
| 상세 → 리뷰 작성 | push |
| 로그인 → 홈 | replace |
| 회원가입 완료 → 로그인 | replace |
| 온보딩 완료 → 메인 | replace |
| 결제 완료 → 완료 화면 | replace 우선 고려 |
| 중복 결제 또는 중복 제출 위험이 매우 큰 경우 | replace + preventBrowserBack 검토 |
12. 마무리
처음에는 특정 페이지로 이동하면서 브라우저 히스토리를 모두 삭제하면 된다고 생각했습니다.
하지만 브라우저는 보안상 웹사이트가 전체 히스토리를 삭제할 수 있는 API를 제공하지 않습니다.
그래서 상황에 따라 다음과 같이 선택할 수 있습니다.
- 일반적인 페이지 이동:
router.push() - 직전 페이지로 돌아가면 안 되는 이동:
router.replace() - 뒤로가기 자체가 위험한 특수 상황:
preventBrowserBack()검토 - 반복되는 이동 로직 관리:
goPage()공통 함수 사용
중요한 점은 모든 페이지에서 뒤로가기를 막는 것이 아니라, 화면의 목적과 사용자 경험을 고려해 적절한 방식을 선택하는 것입니다.