
Vue 애플리케이션이 작을 때는 부모 컴포넌트가 상태를 갖고 props와 emit으로 전달하는 방식만으로 충분합니다. 화면과 컴포넌트가 늘어나면 로그인 사용자, 장바구니, 알림처럼 멀리 떨어진 여러 화면이 공유하는 데이터가 생깁니다. 이때 Pinia를 사용하면 상태와 계산값, 변경 동작을 하나의 저장소로 정리할 수 있습니다. 그러나 모든 데이터를 전역 저장소에 넣으면 오히려 흐름이 복잡해집니다. 핵심은 여러 기능이 실제로 공유하는 상태만 스토어에 두고 나머지는 가장 가까운 컴포넌트가 소유하게 하는 것입니다.
로컬 상태와 전역 상태 구분하기
모달 열림 여부, 현재 입력 중인 폼 값, 한 화면에서만 쓰는 필터는 보통 컴포넌트의 ref나 reactive로 충분합니다. 인증 사용자, 여러 페이지가 함께 쓰는 장바구니, 앱 전체 알림은 Pinia 후보입니다. 서버에서 가져온 데이터라고 모두 전역 상태는 아닙니다. 캐시와 재조회, 만료가 중요한 서버 상태는 전용 데이터 패칭 도구나 composable이 더 적합할 수 있습니다.
state, getters, actions의 역할
state에는 원본 데이터를 둡니다. getters는 state에서 계산할 수 있는 값을 표현하며 Vue의 computed와 비슷하게 동작합니다. 장바구니 합계를 별도 state로 저장하면 상품 수량이 바뀔 때 두 값을 동기화해야 하므로 getter로 계산하는 편이 안전합니다. actions에는 비동기 API 호출과 여러 상태를 함께 변경하는 업무 동작을 둡니다.
스토어 구조 체크포인트
- state는 최소한의 원본 데이터만 유지합니다.
- 파생값은 getters로 계산합니다.
- actions 이름은 setData보다 loadCart, submitOrder처럼 업무 의도를 나타냅니다.
- 한 스토어가 너무 커지면 기능 영역 기준으로 분리합니다.
- 스토어끼리 순환 의존하지 않도록 호출 방향을 정합니다.
스토어 구조 분해 시 반응성 유지하기
스토어의 state와 getters를 일반 구조 분해하면 Vue의 반응성 연결이 끊길 수 있습니다. 템플릿에서 사용할 값은 storeToRefs로 꺼내고 actions는 스토어에서 직접 구조 분해해 사용할 수 있습니다. 단순히 화면에 보인다는 이유로 문제가 없는 것으로 판단하지 말고 값 변경 후 DOM이 갱신되는지 테스트해야 합니다.
실무 원칙: Pinia 스토어는 일반 객체처럼 보여도 반응형 프록시이므로 값을 꺼내는 방식이 업데이트 동작에 영향을 줍니다.
비동기 action의 상태를 명확하게
API를 호출하는 action에는 데이터뿐 아니라 loading과 error 상태가 필요합니다. 요청 시작 전에 오류를 초기화하고 finally에서 로딩을 종료합니다. 같은 요청이 중복 실행되지 않게 현재 진행 여부를 확인하거나 AbortController로 이전 요청을 취소할 수 있습니다. 느린 이전 응답이 나중 요청의 결과를 덮지 않도록 요청 ID나 취소 정책을 사용합니다.
action 안에서 오류를 모두 삼키면 컴포넌트는 성공과 실패를 구분할 수 없습니다. 스토어가 사용자 메시지까지 결정할지, 오류를 다시 던져 화면이 표시할지 팀 규칙을 정합니다. 인증 만료처럼 앱 전체 정책이 필요한 오류와 폼 필드 오류처럼 화면이 처리해야 하는 오류를 분리하는 것이 좋습니다.
스토어에 서버 데이터를 영구 저장할 때 주의하기
새로고침 뒤에도 상태를 유지하려고 localStorage에 스토어 전체를 저장하면 토큰과 개인정보가 노출되고 오래된 데이터가 남을 수 있습니다. 필요한 필드만 선택하고 버전과 만료 시각을 함께 저장합니다. 액세스 토큰의 저장 위치는 공격 모델과 백엔드 구조를 고려해야 하며, 단순히 Pinia 플러그인에 맡긴다고 보안이 해결되지는 않습니다.
저장된 JSON을 복원할 때는 신뢰할 수 없는 외부 입력처럼 검증합니다. 앱 버전이 바뀌어 필드 구조가 달라질 수 있으므로 마이그레이션이나 초기화 정책을 마련합니다. 로그아웃할 때 사용자별 스토어를 명시적으로 초기화하지 않으면 다음 사용자가 이전 데이터를 볼 수 있습니다.
SSR 환경의 요청 간 상태 분리
서버 사이드 렌더링에서는 서버가 여러 사용자의 요청을 처리합니다. 전역 단일 객체에 사용자 상태를 보관하면 요청 사이에 데이터가 섞일 위험이 있습니다. 애플리케이션 요청마다 독립 Pinia 인스턴스를 만들고 서버에서 직렬화한 초기 상태를 안전하게 이스케이프해 클라이언트로 전달합니다. 브라우저 전용 API인 localStorage는 서버 렌더링 단계에서 사용할 수 없으므로 실행 환경을 구분합니다.
스토어 테스트 방법
- 각 테스트에서 새 Pinia 인스턴스를 생성합니다.
- getter가 경계값과 빈 상태에서 올바른지 확인합니다.
- action 성공 시 state 변경을 검증합니다.
- API 실패 시 loading 종료와 error 상태를 확인합니다.
- 동시 요청과 취소 상황을 시험합니다.
- 로그아웃 후 사용자 데이터가 초기화되는지 확인합니다.
- 저장 데이터의 만료와 버전 변경을 테스트합니다.
API 모듈을 action 안에서 직접 고정하면 테스트가 어려울 수 있습니다. 의존성을 함수 인자나 별도 서비스로 분리하고 테스트에서는 가짜 응답을 주입합니다. Pinia의 action을 전부 흉내 내는 대신 중요한 업무 동작은 실제 스토어를 실행해 상태 변화를 확인하는 편이 리팩터링에 강합니다.
Devtools와 운영 관측
Vue Devtools로 action 실행 순서와 상태 변경을 확인하면 예상치 못한 변경 주체를 찾기 쉽습니다. 운영 환경에서는 상태 전체를 로그에 남기지 말고 action 이름, 요청 ID, 처리 시간과 성공 여부 같은 최소 정보만 기록합니다. 특히 사용자와 토큰, 결제 정보가 포함된 state 스냅샷은 오류 추적 도구로 전송되지 않게 필터링합니다.
Pinia를 잘 사용하는 기준은 전역 상태의 양이 많아지는 것이 아닙니다. 어떤 화면이 상태를 소유하고 누가 어떤 action으로 변경하는지 쉽게 설명할 수 있어야 합니다. 로컬 상태와 공유 상태를 나누고, 파생값과 비동기 경계를 명확히 하면 Vue 애플리케이션의 데이터 흐름을 기능이 늘어나도 안정적으로 유지할 수 있습니다.
한 줄 요약: Pinia는 공유 원본 상태만 보관하고 파생값·비동기·저장·초기화 규칙을 명확히 할 때 유지보수성이 높아집니다.