관리자 패널용 Vue 3 상태 관리: Pinia vs 로컬 상태
Vue 3 관리자 패널의 상태 관리를 Pinia, provide/inject, 로컬 상태 중에서 실제 예시(필터, 드래프트, 탭)를 통해 어떻게 선택할지 안내합니다.

관리자 패널에서 상태가 까다로운 이유
관리자 패널은 한 화면에 많은 요소가 얽혀 있어서 상태가 무거워 보입니다. 테이블은 단순 데이터가 아닙니다. 정렬, 필터, 페이징, 선택된 행, 사용자가 의존하는 “방금 무슨 일이 있었지?”라는 맥락까지 포함합니다. 긴 폼, 역할 기반 권한, UI가 허용해야 하는 동작 등까지 더하면 작은 상태 결정이 큰 문제가 됩니다.
문제는 값을 저장하는 것이 아닙니다. 여러 컴포넌트가 같은 진실을 필요로 할 때 동작을 예측 가능하게 유지하는 것이 핵심입니다. 필터 칩이 “Active”라고 표시되면 테이블, URL, 내보내기 동작이 모두 일치해야 합니다. 사용자가 레코드를 편집하고 다른 곳으로 이동하면 앱이 조용히 작업을 잃어버리면 안 됩니다. 탭을 두 개 열면 한 탭이 다른 탭을 덮어써서는 안 됩니다.
Vue 3에서는 보통 상태를 보관할 곳을 세 곳 중에서 선택하게 됩니다:
- 로컬 컴포넌트 상태: 한 컴포넌트가 소유하며 언마운트될 때 안전하게 리셋됩니다.
- provide/inject: prop 전달 없이 페이지나 기능 영역에 범위가 한정된 공유 상태.
- Pinia: 내비게이션을 견디고 라우트 간 재사용되며 디버그하기 쉬운 전역 공유 상태.
유용한 사고 방식은 각 상태 조각에 대해 어디에 두어야 올바르게 유지되고 사용자에게 놀라움을 주지 않으며 스파게티가 되지 않는지를 결정하는 것입니다.
아래 예제는 세 가지 흔한 관리자 문제에 집중합니다: 필터와 테이블(무엇을 유지하고 무엇을 리셋할지), 드래프트와 미저장 편집(사용자가 신뢰할 수 있는 폼), 다중 탭 편집(상태 충돌 방지).
도구를 선택하기 전에 상태를 분류하는 간단한 방법
도구 논쟁은 도구 자체에 집중하기 전 먼저 상태의 종류를 이름으로 부를 때 훨씬 쉬워집니다. 서로 다른 상태 유형은 다르게 행동하며 이들을 섞어 쓰는 것이 이상한 버그를 만듭니다.
실용적인 분류:
- UI 상태: 토글, 열린 다이얼로그, 선택된 행, 활성 탭, 정렬 순서.
- 서버 상태: API 응답, 로딩 플래그, 오류, 마지막 갱신 시간.
- 폼 상태: 필드 값, 검증 오류, 더티 플래그, 미저장 드래프트.
- 스크린 간 상태: 여러 라우트가 읽거나 변경해야 하는 것(현재 워크스페이스, 공유 권한 등).
그다음 범위(scope) 를 정의하세요. 오늘 어디에서 사용되는지를 물어보세요, 언젠가 어디에서 쓰일지 말고요. 한 테이블 컴포넌트 내에서만 중요하면 로컬 상태로 충분합니다. 같은 페이지의 두 형제 컴포넌트가 필요하면 실제 문제는 페이지 레벨 공유입니다. 여러 라우트가 필요하면 앱 공유 상태 영역에 들어갑니다.
다음은 수명(lifetime) 입니다. 어떤 상태는 드로어를 닫으면 리셋되어야 합니다. 다른 상태는 내비게이션을 견뎌야 합니다(레코드로 들어갔다가 돌아올 때 필터 유지). 어떤 것은 리로드를 견뎌야 합니다(사용자가 나중에 돌아오는 긴 드래프트). 이 세 가지를 모두 같은 방식으로 다루면 필터가 이상하게 리셋되거나 드래프트가 사라지는 일이 발생합니다.
마지막으로 동시성(concurrency) 을 확인하세요. 관리자 패널은 엣지 케이스에 자주 부딪힙니다: 사용자가 같은 레코드를 두 탭에서 열고, 백그라운드 리프레시가 폼이 더티인 동안 행을 업데이트하거나, 두 편집자가 동시에 저장을 시도하는 경우 등입니다.
예: 필터, 테이블, 편집 드로어가 있는 “Users” 화면. 필터는 페이지 수명의 UI 상태이고, 행은 서버 상태입니다. 드로어 필드는 폼 상태입니다. 같은 사용자가 두 탭에서 편집되면 블록, 병합, 경고 중 명확한 동시성 결정을 내려야 합니다.
상태를 유형, 범위, 수명, 동시성으로 라벨링할 수 있으면 도구 선택(local, provide/inject, Pinia)은 보통 훨씬 명확해집니다.
선택 방법: 오래 견디는 의사결정 프로세스
좋은 상태 선택은 한 가지 습관에서 시작합니다: 도구를 고르기 전에 상태를 평범한 말로 묘사하세요. 관리자 패널은 테이블, 필터, 큰 폼, 레코드 간 내비게이션을 섞어 쓰므로 “작은” 상태도 버그 유발자가 될 수 있습니다.
5단계 의사결정 프로세스
-
누가 이 상태를 필요로 하나?
- 한 컴포넌트: 로컬로 유지하세요.
- 한 페이지 아래의 여러 컴포넌트:
provide/inject고려. - 여러 라우트: Pinia 고려.
필터가 좋은 예입니다. 필터가 그 테이블 하나에만 영향을 주면 로컬이면 충분합니다. 헤더 컴포넌트에 있고 아래 테이블을 드라이브하면 페이지 범위 공유(대개
provide/inject)가 깔끔합니다. -
얼마나 오래 살아야 하나?
- 컴포넌트 언마운트 시 사라져도 되면 로컬이 이상적입니다.
- 라우트 변경을 견뎌야 하면 Pinia가 더 적합합니다.
- 리로드도 견뎌야 하면 위치에 상관없이 영속성(storage)이 필요합니다.
이건 드래프트에서 특히 중요합니다. 미저장 편집은 신뢰에 민감합니다: 사용자는 클릭해 나갔다가 돌아왔을 때 드래프트가 남아있기를 기대합니다.
-
브라우저 탭 간에 공유되어야 하나, 탭마다 격리되어야 하나?
다중 탭 편집에서 버그가 숨어 있습니다. 각 탭이 자체 드래프트를 가져야 하면 단일 글로벌 싱글턴을 피하세요. 레코드 ID로 키를 지정하거나 페이지 범위로 유지해 한 탭이 다른 탭을 덮어쓰지 않게 하세요.
-
적합한 가장 단순한 옵션을 선택하세요.
로컬에서 시작하세요. prop 드릴링, 중복 로직, 재현하기 힘든 리셋이 느껴질 때만 위로 올리세요.
-
디버깅 필요성을 확인하세요.
변경 사항을 명확히 보고 싶다면 Pinia의 중앙 집중형 액션과 상태 검사 기능이 큰 시간을 절약합니다. 상태가 단기적이고 명확하면 로컬 상태가 읽기 쉽습니다.
로컬 컴포넌트 상태: 언제 충분한가
로컬 상태는 데이터가 한 페이지의 한 컴포넌트에서만 중요할 때 기본 선택입니다. 이 옵션을 건너뛰고 처음부터 과도한 스토어를 만들면 몇 달간 유지보수하는 부담을 떠안게 됩니다.
명확한 적합 사례는 자체 필터가 있는 단일 테이블입니다. 필터가 한 테이블(예: Users 리스트)에만 영향을 주고 다른 곳이 의존하지 않으면 테이블 컴포넌트 내부의 ref 값으로 유지하세요. “모달이 열려 있는가?”, “어떤 행을 편집 중인가?”, “지금 어떤 항목이 선택되었는가?” 같은 작은 UI 상태도 마찬가지입니다.
계산 가능한 것은 저장하지 마세요. “Active filters (3)” 배지는 현재 필터 값에서 계산하는 것이 좋습니다. 정렬 라벨, 포맷된 요약, “저장 가능” 플래그도 computed로 두면 자동으로 동기화됩니다.
리셋 규칙은 도구보다 더 중요할 때가 많습니다. 무엇이 라우트 변경 시 지워지고, 무엇이 같은 페이지 내 뷰 전환 시 남아야 하는지 결정하세요(예: 필터는 유지하지만 임시 선택은 지워서 의도치 않은 대량 액션을 피할 수 있음).
로컬 컴포넌트 상태가 보통 충분한 경우:
- 상태가 한 위젯(한 폼, 한 테이블, 한 모달)에만 영향을 줄 때.
- 다른 화면이 읽거나 변경할 필요가 없을 때.
- 1–2개 컴포넌트 내에 prop을 통과시키지 않고 유지할 수 있을 때.
- 리셋 동작을 한 문장으로 설명할 수 있을 때.
주된 한계는 깊이입니다. 동일한 상태를 여러 중첩 컴포넌트로 전달하기 시작하면 로컬 상태가 prop 드릴링으로 변하고, 그때가 provide/inject나 스토어로 옮길 신호입니다.
provide/inject: 페이지나 기능 영역 내 상태 공유
provide/inject는 로컬 상태와 전체 스토어의 중간에 위치합니다. 부모가 값을 제공(provide)하고 자손들이 주입(inject)해 prop 드릴링 없이 접근합니다. 관리자 패널에서는 상태가 한 화면이나 기능 영역에 속할 때 훌륭한 선택입니다.
흔한 패턴은 페이지 셸이 상태를 소유하고 작은 컴포넌트들이 그것을 소비하는 방식입니다: 필터 바, 테이블, 일괄 작업 툴바, 상세 드로어, “미저장 변경 있음” 배너 등. 셸은 filters 객체, draftStatus 객체(더티, 저장 중, 오류), 그리고 권한 기반 isReadOnly 같은 읽기 전용 플래그를 제공할 수 있습니다.
무엇을 제공할지(작게 유지)
모든 것을 제공하면 구조가 덜 정리된 스토어를 재현한 셈이 됩니다. 여러 자식이 진짜로 필요로 하는 것만 제공하세요. 필터는 고전적인 예입니다: 테이블, 칩, 내보내기 동작, 페이징이 동기화되어야 할 때 하나의 진실 소스를 공유하는 것이 여러 props와 이벤트를 주고받는 것보다 낫습니다.
명확성과 함정
가장 큰 위험은 숨겨진 의존성입니다: 자식 컴포넌트가 상위에서 제공된 데이터 때문에 “그냥 작동”하고 나중에 업데이트가 어디서 오는지 알기 어려워지는 경우입니다.
가독성과 테스트 용이성을 위해 주입에는 명확한 이름(보통 상수나 Symbol 사용)을 주고, 단순한 API를 제공하는 것이 좋습니다. setFilter, markDirty, resetDraft 같은 작은 액션 API는 소유권과 허용된 변경을 명시적으로 만듭니다.
Pinia: 화면 간 공유와 예측 가능한 업데이트
Pinia는 동일한 상태가 라우트를 넘나들며 일관되어야 할 때 강력합니다. 관리자 패널에서는 현재 사용자, 사용 권한, 선택된 조직/워크스페이스, 앱 수준 설정 등이 흔한 사례입니다. 각 화면이 같은 것을 다시 구현하면 고통스러워집니다.
스토어는 읽고 업데이트할 중앙 장소를 제공합니다. props를 여러 레이어로 전달하는 대신 필요할 때 스토어를 임포트하면 됩니다. 리스트에서 상세 페이지로 이동해도 UI의 나머지 부분은 같은 선택된 조직, 권한, 설정에 반응할 수 있습니다.
Pinia가 유지보수에 쉬운 이유
Pinia는 간단한 구조를 권장합니다: 원시 값은 state, 파생 값은 getters, 업데이트는 actions. 관리자 UI에서는 이 구조가 임시 수정이 흩어져 버그로 번지는 것을 막습니다.
canEditUsers가 현재 역할과 기능 플래그에 따라 달라진다면 getter에 규칙을 넣으세요. 조직 전환 시 캐시된 선택을 지우고 내비게이션을 다시 로드해야 하면 그 순서를 action에 넣으세요. 그러면 수수께끼 같은 워처나 “왜 이게 변했지?” 문제가 줄어듭니다.
Pinia는 Vue DevTools와도 잘 동작합니다. 버그가 발생하면 스토어 상태를 검사하고 어떤 action이 실행됐는지 보는 것이 무작위로 생성된 반응형 객체들에서 변경을 추적하는 것보다 훨씬 쉽습니다.
쓰레기통 스토어를 피하라
전역 스토어는 처음엔 깔끔해 보이지만 곧 잡동사니가 쌓입니다. Pinia에 적합한 항목은 사용자 식별, 권한, 선택된 워크스페이스, 기능 플래그, 여러 화면에서 쓰이는 참조 데이터 같은 진정으로 공유되는 관심사입니다.
페이지 전용 관심사(하나의 폼 임시 입력 등)는 여러 라우트가 진짜로 필요하지 않는 한 로컬에 두세요.
예시 1: 모든 것을 스토어로 만들지 않고 필터와 테이블 다루기
주문 페이지를 상상해 보세요: 테이블, (상태, 날짜 범위, 고객) 같은 필터, 페이징, 선택된 주문을 미리보는 사이드 패널. 모든 필터와 테이블 설정을 전역 스토어에 넣고 싶은 유혹이 쉽습니다.
간단한 선택 기준은 무엇을 기억해야 하고 어디에 둘지 결정하는 것입니다:
- 메모리 전용(local 또는 provide/inject): 페이지를 떠나면 리셋. 일시적 상태에 적합.
- 쿼리 파라미터: 공유 가능하고 리로드를 견딤. 사람들이 필터와 페이징을 복사할 때 유용.
- Pinia: 내비게이션을 견디고 라우트 간에 유지. “떠난 대로 리스트로 돌아오기”에 적합.
구현은 보통 이 규칙을 따릅니다:
누군가 설정이 내비게이션을 견디길 기대하지 않으면 filters, sort, page, pageSize를 Orders 페이지 컴포넌트 안에 두고 그 페이지가 페치를 트리거하게 하세요. 툴바, 테이블, 미리보기 패널이 같은 모델을 필요로 해서 prop 전달이 번거로워지면 리스트 모델을 페이지 셸로 옮겨 provide/inject로 공유하세요. 라우트 간에 리스트를 그대로 유지하길 원하면 Pinia가 더 적합합니다.
실용 규칙: 로컬에서 시작하고 여러 자식 컴포넌트가 같은 모델을 필요로 하면 provide/inject로 올리세요. 진짜로 크로스 라우트 지속성이 필요할 때만 Pinia를 사용하세요.
예시 2: 드래프트와 미저장 편집(사용자가 신뢰하는 폼)
지원 담당자가 고객 레코드(연락처, 결제 정보, 내부 메모)를 편집하는 상황을 생각해 보세요. 중간에 방해를 받고 화면을 바꾼 뒤 돌아왔을 때 폼이 작업을 잊거나 반쪽짜리 데이터가 저장되면 신뢰는 사라집니다.
드래프트는 세 가지를 분리하세요: 마지막 저장된 레코드, 사용자가 단계적으로 편집한 내용(스테이지된 에디트), 그리고 검증 오류 같은 UI 전용 상태.
로컬 상태: 명확한 더티 규칙을 가진 스테이지된 편집
편집 화면이 자체적으로 완결되면 로컬 컴포넌트 상태가 가장 안전한 경우가 많습니다. 레코드를 불러오고, 드래프트 복사본을 만들고, 드래프트를 편집하고, 사용자가 Save를 클릭할 때만 저장 요청을 보내세요. Cancel은 드래프트를 버리고 다시 로드합니다.
provide/inject: 중첩된 섹션 간 단일 드래프트 공유
관리자 폼은 탭이나 패널로 나뉘는 경우가 많습니다(프로필, 주소, 권한). provide/inject로 하나의 드래프트 모델을 유지하고 updateField(), resetDraft(), validateSection() 같은 작은 API를 노출하면 각 섹션이 다섯 단계의 prop 전달 없이 동일한 드래프트를 읽고 쓸 수 있습니다.
드래프트에 Pinia가 도움이 되는 경우
드래프트를 라우트 간에 유지하거나 편집 화면 밖에서도 보여줘야 하면 Pinia가 유용합니다. 흔한 패턴은 draftsById[customerId]처럼 각 레코드에 고유 드래프트를 두는 것입니다. 여러 편집 화면을 동시에 열 수 있을 때도 도움이 됩니다.
드래프트 버그는 몇 가지 예측 가능한 실수에서 나옵니다: 레코드가 로드되기 전에 드래프트를 만들기, 리페치 시 더티인 드래프트를 덮어쓰기, 취소 시 오류를 지우지 않기, 하나의 공유 키로 드래프트가 서로 덮어쓰게 하는 것 등. 언제 생성하고, 덮어쓰고, 폐기하고, 지속하고, 저장 후 교체할지에 대한 명확한 규칙을 세우면 대부분 사라집니다.
AppMaster로 관리자 화면을 빌드하더라도 “드래프트 대 저장된 레코드” 분리는 동일하게 적용됩니다: 드래프트는 클라이언트에 두고 Save가 성공한 뒤에만 백엔드를 진실 소스로 대하세요.
예시 3: 상태 충돌 없는 다중 탭 편집
다중 탭 편집은 관리자 패널이 종종 깨지는 지점입니다. 사용자가 고객 A를 열고 고객 B를 연 뒤 다시 왔다갔다할 때 각 탭이 자체 미저장 변경을 기억하길 기대합니다.
해결책은 각 탭을 하나의 상태 번들로 모델링하는 것입니다. 각 탭은 보통 레코드 ID(또는 탭 키), 드래프트 데이터, 상태(clean, dirty, saving), 필드 오류를 가져야 합니다.
탭이 한 화면 안에 있으면 로컬 접근이 잘 작동합니다. 탭 목록과 드래프트를 렌더하는 페이지 컴포넌트가 소유하세요. 각 에디터 패널은 자신만의 번들만 읽고 씁니다. 탭을 닫으면 그 번들을 삭제하면 됩니다. 이렇게 하면 격리되어 이해하기 쉬워집니다.
어디에 있든 형태는 비슷합니다:
- 탭 객체 리스트(각각
customerId,draft,status,errors포함) activeTabKeyopenTab(id),updateDraft(key, patch),saveTab(key),closeTab(key)같은 액션
탭이 내비게이션을 견뎌야 하거나 여러 화면이 탭을 열고 포커스해야 하면 Pinia가 더 낫습니다. 그 경우 작은 “탭 매니저” 스토어가 앱 전반에서 일관된 동작을 유지합니다.
피해야 할 주요 충돌은 currentDraft 같은 단일 전역 변수를 사용하는 것입니다. 두 번째 탭이 열리면 서로 덮어쓰고 검증 오류가 잘못된 곳에 표시되며 저장이 잘못된 레코드를 업데이트하는 상황이 발생합니다. 열린 탭마다 별도 번들을 가지면 충돌은 설계상 대부분 사라집니다.
버그와 지저분한 코드의 흔한 실수
대부분의 관리자 패널 버그는 “Vue 버그”가 아닙니다. 상태 버그입니다: 데이터가 잘못된 곳에 있고, 화면의 두 부분이 일치하지 않거나, 오래된 상태가 조용히 남아 있습니다.
가장 흔한 패턴들은 다음과 같습니다:
모든 것을 기본으로 Pinia에 넣으면 소유권이 불분명해집니다. 전역 스토어는 처음엔 정리된 것처럼 보이지만 곧 모든 페이지가 같은 객체를 읽고 쓰게 되어 정리와 정리가 힘들어집니다.
provide/inject를 명확한 계약 없이 사용하면 숨겨진 의존성이 생깁니다. 자식이 filters를 주입하는데 누가 제공하는지, 어떤 액션이 변경할 수 있는지에 대한 합의가 없으면 다른 자식이 같은 객체를 변형할 때 놀라운 업데이트가 발생합니다.
서버 상태와 UI 상태를 같은 스토어에 섞으면 우발적 덮어쓰기가 생깁니다. 페치한 레코드는 “드로어가 열려 있는가?”, “현재 탭” 또는 “더티 필드”와 다르게 동작합니다. 같이 두면 리페치가 UI를 덮어쓰거나 UI 변경이 캐시된 데이터를 변형할 수 있습니다.
라이프사이클 정리를 건너뛰면 상태가 새어나갑니다. 한 뷰의 필터가 다른 뷰에 영향을 줄 수 있고 드래프트가 페이지를 떠난 뒤에도 남을 수 있습니다. 다음에 다른 레코드를 열면 오래된 선택이 표시되어 사용자는 앱이 고장났다고 생각할 수 있습니다.
드래프트 키를 잘못 지정하는 것은 조용한 신뢰 훼손입니다. draft:editUser 같은 하나의 키 아래에 드래프트를 저장하면 User A를 편집한 뒤 User B를 편집할 때 같은 드래프트가 덮어써집니다.
간단한 규칙으로 대부분을 예방할 수 있습니다: 상태를 가능한 사용되는 곳 가까이에 두고, 두 독립된 파트가 진짜로 공유해야 할 때만 끌어올리세요. 공유할 때는 누가 소유권을 가지는지(누가 변경할 수 있는지)와 정체성(어떻게 키를 지정할지)을 정의하세요.
로컬, provide/inject, Pinia를 선택하기 전 빠른 체크리스트
가장 유용한 질문은: 누가 이 상태를 소유하나? 한 문장으로 말할 수 없으면 그 상태는 아마도 너무 많은 일을 하고 있어 분리해야 합니다.
빠른 필터로 이 체크를 사용하세요:
- 소유자를 이름으로 말할 수 있나요(컴포넌트, 페이지, 앱 전체)?
- 라우트 변경이나 리로드를 견뎌야 하나? 그렇다면 브라우저가 알아서 유지해 주길 바라기보다 영속성을 계획하세요.
- 두 레코드를 동시에 편집할 일이 있나요? 있다면 레코드 ID로 상태를 키하세요.
- 상태가 한 페이지 셸 아래의 컴포넌트만 사용하나요? 그렇다면
provide/inject가 적합할 수 있습니다. - 변경을 검사하고 누가 무엇을 변경했는지 이해할 필요가 있나요? 그렇다면 Pinia가 그 슬라이스를 담기에 깨끗한 곳일 때가 많습니다.
도구 매칭을 단순히 말하면:
상태가 한 컴포넌트 안에서만 생성되고 소멸하면(예: 드롭다운 열림/닫힘 플래그) 로컬로 두세요. 같은 화면의 여러 컴포넌트가 공유 컨텍스트가 필요하면 provide/inject가 전역화하지 않고 공유하게 해줍니다. 상태가 화면 간 공유되어야 하고 내비게이션을 견뎌야 하며 예측 가능하고 디버그 가능한 업데이트가 필요하면 Pinia를 사용하고 드래프트가 관련되면 레코드 ID로 항목을 키하세요.
AppMaster로 Vue 3 관리자 UI를 빌드한다면 이 체크리스트가 모든 것을 너무 빨리 스토어에 넣는 실수를 피하도록 도와줄 것입니다.
다음 단계: 상태를 어지럽히지 않고 진화시키기
관리자 패널의 상태 관리를 개선하는 가장 안전한 방법은 작고 지루한 단계로 확장하는 것입니다. 한 페이지 안에 머무르는 것들은 먼저 로컬로 시작하세요. 진짜 재사용(복사된 로직, 세 번째 컴포넌트가 같은 상태 필요)이 보이면 한 레벨 올리세요. 그제서야 공유 스토어를 고려하세요.
대부분 팀에 통하는 경로:
- 페이지 전용 상태는 먼저 로컬에 두세요(필터, 정렬, 페이징, 열림/닫힘 패널).
- 같은 페이지의 여러 컴포넌트가 공유 컨텍스트를 필요로 하면
provide/inject를 사용하세요. - 크로스스크린 필요가 있을 때만 한 번에 하나씩 Pinia 스토어를 추가하세요(드래프트 매니저, 탭 매니저, 현재 워크스페이스).
- 리셋 규칙을 작성하고 지키세요(내비게이션, 로그아웃, Clear filters, Discard changes 시 무엇을 리셋할지).
리셋 규칙은 작아 보이지만 대부분의 “왜 이게 변했지?” 순간을 막아줍니다. 예를 들어 누군가 다른 레코드를 열고 돌아왔을 때 드래프트에 대해 복원할지, 경고할지, 리셋할지 결정하고 그 동작을 일관되게 만드세요.
스토어를 도입하면 기능 단위로 모양을 갖추게 하세요. 드래프트 스토어는 생성, 복원, 삭제를 처리해야지 테이블 필터나 UI 레이아웃 플래그까지 책임지면 안 됩니다.
빠르게 관리자 패널을 프로토타입하려면 AppMaster (appmaster.io)가 Vue3 웹 앱과 백엔드 및 비즈니스 로직을 생성해 주며, 생성된 코드를 필요한 부분에서 다듬을 수 있습니다. 실용적인 다음 단계는 화면 하나를 엔드투엔드로 만들어(예: 드래프트 복구 규칙이 포함된 편집 폼) 어떤 것이 진짜로 Pinia가 필요한지, 무엇이 로컬로 남을 수 있는지 확인해보는 것입니다.
자주 묻는 질문
데이터가 한 컴포넌트에만 영향을 주고 그 컴포넌트가 언마운트될 때 초기화돼도 괜찮다면 로컬 상태를 사용하세요. 일반적인 예는 다이얼로그 열기/닫기, 한 테이블의 선택된 행, 재사용되지 않는 폼 섹션 등입니다.
provide/inject는 같은 페이지의 여러 컴포넌트가 하나의 진실 소스를 필요로 하고 prop 전달이 번거로울 때 유용합니다. 제공하는 값을 작고 의도적으로 유지해 페이지를 이해하기 쉽게 만드세요.
상태가 라우트를 넘어서 공유되어야 하거나 내비게이션을 견뎌야 하거나 한 곳에서 쉽게 검사하고 디버그해야 한다면 Pinia를 사용하세요. 흔한 예로 현재 워크스페이스, 권한, 기능 플래그, 드래프트나 탭 같은 크로스스크린 매니저가 있습니다.
먼저 상태 유형을 이름으로 붙이세요(예: UI, 서버, 폼, 크로스스크린). 그다음 범위(한 컴포넌트/한 페이지/여러 라우트), 수명(언마운트 시 리셋/내비게이션을 견딤/리로드를 견딤), 동시성(단일 편집기인지 다중 탭인지)를 결정하세요. 이 네 가지 레이블에서 도구 선택이 자연스럽게 따라옵니다.
사용자가 뷰를 공유하거나 복원하길 기대하면 필터와 페이징을 쿼리 파라미터에 넣어 리로드를 견디게 하세요. 라우트 간에 ‘떠난 대로 돌아오기’를 기대하면 리스트 모델을 Pinia에 저장하세요. 그 외에는 페이지 범위에 두는 것이 좋습니다.
마지막으로 저장된 레코드와 사용자의 임시 편집을 분리하고, 저장 버튼을 누를 때만 백엔드에 반영하세요. 분명한 더티 규칙을 추적하고 내비게이션 시 동작(경고, 자동 저장, 복구 가능한 드래프트 유지 등)을 결정해 사용자가 작업을 잃지 않도록 하세요.
각 열린 편집기는 기록 ID(때로는 탭 키 기반)의 고유 키로 관리되는 자체 상태 번들을 가져야 합니다. currentDraft 같은 전역 변수를 쓰지 마세요. 탭마다 별도 번들을 가지면 편집 충돌과 잘못된 검증 오류 표시를 방지할 수 있습니다.
편집 흐름이 하나의 라우트에 갇혀 있다면 페이지 소유의 provide/inject 방식이 작동할 수 있습니다. 드래프트를 라우트 간에 유지하거나 편집 화면 외부에서 접근해야 하면 Pinia에서 draftsById[recordId] 같은 구조로 관리하는 것이 더 간단하고 예측 가능합니다.
계산으로 만들 수 있는 값은 저장하지 마세요. 배지, 요약, “저장 가능” 플래그 등은 computed로 파생하면 상태가 어긋날 일이 줄어듭니다.
기본적으로 모든 것을 Pinia에 넣는 것, 서버 응답과 UI 토글을 같은 곳에 섞는 것, 내비게이션 시 정리하지 않는 것이 가장 흔한 실수입니다. 또한 레코드마다 하나의 공유 키로 드래프트를 저장하면 서로 덮어쓰는 문제가 생기므로 키 지정에 주의하세요.


