Pinia의 좀비 상태: 이미 겪고 있을지도 모르는 숨은 버그

발행: (2025년 12월 20일 오전 07:02 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

좀비 상태란?

  • 페이지, 사용자 흐름, 완료된 폼, 혹은 끝난 비동기 요청이 끝난 뒤에도 남아 있는 상태.
  • 여전히 다음을 할 수 있다:
    • 입력값을 미리 채우기
    • 검증 트리거
    • UI 결정에 영향 주기
    • “무작위” 버그 발생

Pinia 스토어는 기본적으로 전역, 장수 싱글톤이기 때문에, 페이지 이동만으로는 스토어 데이터가 초기화되지 않는다. 명시적으로 리셋하지 않으면, 더 이상 의미가 없어도 상태가 메모리에 남아 있다.

아주 흔한 예시

// useWizardStore.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useWizardStore = defineStore('wizard', () => {
  const step = ref(2)
  const email = ref('old@email.com')

  return { step, email }
})
  1. 사용자가 마법사를 연다.
  2. 마법사는 2단계에서 시작하고 이전 이메일을 보여준다.
  3. 검증이 예상치 못하게 실패한다.

스토어가 절대 리셋되지 않아 좀비 상태가 생성된다.

비동기와 관련된 좀비 상태

async function loadUser() {
  user.value = await fetchUser()
}

비동기 응답이 컴포넌트가 언마운트된 뒤 혹은 컨텍스트가 바뀐 뒤에 도착하면, 오래된 데이터가 현재 상태를 덮어써서 같은 문제가 발생한다.

주의해야 할 증상

  • 채워져서는 안 되는 폼이 이미 채워져 있음
  • 첫 렌더링 시 검증 오류 발생
  • 네비게이션(뒤로/앞으로) 후 잘못된 데이터 표시
  • 네비게이션 후 UI가 다르게 동작
  • 페이지 새로고침 시 사라지는 버그

이러한 버그는 현재 상태가 아니라 네비게이션 히스토리에 의존하기 때문에 디버깅이 어렵다.

좀비 상태 방지 방법

1. 명시적인 리셋 추가

// useWizardStore.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useWizardStore = defineStore('wizard', () => {
  const step = ref(1)
  const email = ref('')

  function reset() {
    step.value = 1
    email.value = ''
  }

  return { step, email, reset }
})
import { onUnmounted } from 'vue'
import { useWizardStore } from '@/stores/useWizardStore'

const store = useWizardStore()
onUnmounted(() => store.reset())

Pinia가 자동으로 상태를 리셋해 주지는 않는다—직접 의도적으로 해야 한다.

2. 컨텍스트 변경 시 리셋

import { watch } from 'vue'
import { useRoute } from 'vue-router'
import { useWizardStore } from '@/stores/useWizardStore'

const route = useRoute()
const store = useWizardStore()

watch(
  () => route.params.id,
  () => store.reset()
)

이렇게 하면 오래된 데이터가 새로운 컨텍스트에 섞이는 것을 방지한다.

3. 비동기 요청 보호

let requestId = 0

async function load() {
  const id = ++requestId
  const data = await fetchData()

  if (id !== requestId) return // 오래된 응답 무시
  state.value = data
}

늦게 도착한 응답은 무시하고 유효한 상태를 덮어쓰지 않는다.

4. UI 상태와 도메인 상태 분리

  • 스토어에 적합한 후보: 사용자 데이터, 인증 정보, 공유 도메인 엔티티.
  • 스토어에 부적합한 후보: 단명 UI 상태(예: 모달 표시 여부, 일시적인 폼 필드). 이런 것은 컴포넌트에 가깝게 두자.

오래된 상태의 종류

TypeMeaning
Stale state오래됐지만 여전히 유효
Zombie state무효하지만 여전히 활성

결론

좀비 상태는 보기에는 정상적이지만 실제로는 그렇지 않다. 상태가 소유자를 초과해 살아남으면 좀비가 된다. 이 문제는 Pinia에만 국한된 것이 아니라 상태‑수명 주기 문제이다.

Pinia는 강력한 도구를 제공하지만, 그만큼 책임도 따른다:

  • 소유권 정의
  • 수명 관리
  • 의도적인 리셋

이러한 관행을 적용하면 다음과 같은 애플리케이션을 만들 수 있다:

  • 이해하기 쉬움
  • 디버깅이 쉬움
  • 사용자에게 더 예측 가능함

이 글이 한 명이라도 어려운 프로덕션 버그를 피하는 데 도움이 된다면 공유할 가치가 있다.

Back to Blog

관련 글

더 보기 »