Skip to content

소개

Pinia는 2019년 11월경 Composition API를 활용해 Vue의 스토어를 새롭게 설계하려는 실험으로 시작되었습니다. 그 이후로 초기 원칙은 그대로 유지되었고, 2025년에는 Vue 2 지원이 중단되었지만, Pinia는 Composition API를 반드시 사용할 필요는 없습니다.

왜 Pinia를 사용해야 하나요?

Pinia는 Vue를 위한 스토어 라이브러리로, 컴포넌트/페이지 간에 상태를 공유할 수 있게 해줍니다. Composition API에 익숙하다면, 단순히 export const state = reactive({})로도 전역 상태를 공유할 수 있다고 생각할 수 있습니다. 이는 단일 페이지 애플리케이션(SPA)에서는 맞는 말이지만, 서버 사이드 렌더링(SSR)일 경우 보안 취약점에 노출될 수 있습니다. 하지만 작은 SPA에서도 Pinia를 사용하면 많은 이점을 얻을 수 있습니다:

  • 테스트 유틸리티
  • 플러그인: 플러그인으로 Pinia 기능 확장
  • 적절한 TypeScript 지원 또는 JS 사용자에게 자동완성
  • 서버 사이드 렌더링 지원
  • 개발자 도구 지원
    • 액션과 변이를 추적할 수 있는 타임라인
    • 스토어가 사용된 컴포넌트에 표시됨
    • 타임 트래블 및 더 쉬운 디버깅
  • 핫 모듈 교체
    • 페이지를 새로고침하지 않고 스토어 수정
    • 개발 중에도 기존 상태 유지

아직도 의문이 있다면, 공식 Mastering Pinia 강좌를 확인해보세요. 처음에는 직접 defineStore() 함수를 만드는 방법을 다루고, 이후 공식 Pinia API로 넘어갑니다.

Vue Mastery Logo Get the Pinia Cheat Sheet from Vue Mastery

기본 예제

Pinia를 사용하는 API는 다음과 같습니다(시작하기에서 전체 지침을 꼭 확인하세요). 먼저 스토어를 생성합니다:

js
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // 이렇게도 정의할 수 있습니다
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})

그리고 컴포넌트에서 사용 합니다:

vue
<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

counter.count++
// 자동완성 지원 ✨
counter.$patch({ count: counter.count + 1 })
// 또는 액션을 사용
counter.increment()
</script>

<template>
  <!-- 스토어에서 상태를 직접 접근 -->
  <div>현재 카운트: {{ counter.count }}</div>
</template>

Playground에서 직접 해보기

더 고급 사용 사례를 위해 컴포넌트의 setup()과 유사하게 함수로 스토어를 정의할 수도 있습니다:

js
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})

Playground에서 직접 해보기

아직 setup()과 Composition API가 익숙하지 않더라도 걱정하지 마세요. Pinia는 map 헬퍼 (Vuex와 유사)도 지원합니다. 스토어는 동일하게 정의하고, 이후 mapStores(), mapState(), mapActions()를 사용합니다:

js
const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

const useUserStore = defineStore('user', {
  // ...
})

export default defineComponent({
  computed: {
    // 다른 계산 속성들
    // ...
    // this.counterStore와 this.userStore에 접근 가능
    ...mapStores(useCounterStore, useUserStore),
    // this.count와 this.double에 읽기 접근 가능
    ...mapState(useCounterStore, ['count', 'double']),
  },
  methods: {
    // this.increment()에 접근 가능
    ...mapActions(useCounterStore, ['increment']),
  },
})

Playground에서 직접 해보기

각 _map 헬퍼_에 대한 더 많은 정보는 핵심 개념에서 확인할 수 있습니다.

공식 강좌

Pinia의 공식 강좌는 Mastering Pinia입니다. Pinia의 저자가 직접 작성했으며, 기본부터 플러그인, 테스트, 서버 사이드 렌더링과 같은 고급 주제까지 모두 다룹니다. Pinia를 시작하고 마스터하는 가장 좋은 방법입니다.

Pinia 인가

Pinia(발음은 /piːnjʌ/, 영어로 "피냐"와 비슷)는 piña (스페인어로 파인애플)와 가장 가까운, 유효한 패키지 이름입니다. 파인애플은 실제로 개별 꽃들이 모여 하나의 복합 과일을 이루는 구조입니다. 각각의 스토어가 개별적으로 태어나지만, 결국 모두 연결된다는 점에서 유사합니다. 또한 남아메리카가 원산지인 맛있는 열대 과일이기도 합니다.

더 현실적인 예제

여기 Pinia의 API를 JavaScript에서도 타입과 함께 사용할 수 있는 더 완성도 높은 예제가 있습니다. 어떤 분들에게는 이 예제만으로도 바로 시작할 수 있겠지만, 나머지 문서도 꼭 확인하거나, 이 예제를 건너뛰고 _핵심 개념_을 모두 읽은 후 다시 돌아오는 것을 추천합니다.

js
import { defineStore } from 'pinia'

export const useTodos = defineStore('todos', {
  state: () => ({
    /** @type {{ text: string, id: number, isFinished: boolean }[]} */
    todos: [],
    /** @type {'all' | 'finished' | 'unfinished'} */
    filter: 'all',
    // 타입은 자동으로 number로 추론됩니다
    nextId: 0,
  }),
  getters: {
    finishedTodos(state) {
      // 자동완성! ✨
      return state.todos.filter((todo) => todo.isFinished)
    },
    unfinishedTodos(state) {
      return state.todos.filter((todo) => !todo.isFinished)
    },
    /**
     * @returns {{ text: string, id: number, isFinished: boolean }[]}
     */
    filteredTodos(state) {
      if (this.filter === 'finished') {
        // 자동완성으로 다른 getter 호출 ✨
        return this.finishedTodos
      } else if (this.filter === 'unfinished') {
        return this.unfinishedTodos
      }
      return this.todos
    },
  },
  actions: {
    // 인자 개수 제한 없음, 프로미스 반환 여부도 자유
    addTodo(text) {
      // 상태를 직접 변경할 수 있습니다
      this.todos.push({ text, id: this.nextId++, isFinished: false })
    },
  },
})

Playground에서 직접 해보기

Vuex와의 비교

Pinia는 Vuex의 차기 버전이 어떤 모습일지 탐구하는 과정에서 시작되었으며, Vuex 5를 위한 핵심 팀 논의에서 나온 많은 아이디어를 통합했습니다. 결국 Pinia가 우리가 Vuex 5에서 원했던 대부분을 이미 구현하고 있다는 것을 깨닫고, Pinia를 새로운 권장 사항으로 삼기로 결정했습니다.

Vuex와 비교했을 때, Pinia는 더 간단한 API와 적은 형식적 절차, Composition-API 스타일의 API, 그리고 TypeScript 사용 시 강력한 타입 추론 지원을 제공합니다.

RFC

초기에는 Pinia가 별도의 RFC 과정을 거치지 않았습니다. 애플리케이션 개발 경험, 다른 사람의 코드 읽기, Pinia를 사용하는 클라이언트와의 작업, Discord에서 질문에 답변하는 경험을 바탕으로 아이디어를 실험했습니다. 이 덕분에 다양한 상황과 애플리케이션 크기에 맞는 솔루션을 제공할 수 있었습니다. 자주 배포하며 핵심 API는 그대로 유지한 채 라이브러리를 발전시켰습니다.

이제 Pinia가 기본 상태 관리 솔루션이 된 만큼, Vue 생태계의 다른 핵심 라이브러리와 동일한 RFC 과정을 거치며, API도 안정적인 상태에 들어섰습니다.

Vuex 3.x/4.x와의 비교

Vuex 3.x는 Vue 2용, Vuex 4.x는 Vue 3용입니다

Pinia API는 Vuex ≤4와 매우 다릅니다. 주요 차이점은 다음과 같습니다:

  • _mutations_가 더 이상 존재하지 않습니다. 이는 종종 매우 장황하다고 여겨졌습니다. 초기에는 개발자 도구 통합을 위해 도입되었으나, 이제는 더 이상 문제가 되지 않습니다.
  • TypeScript 지원을 위해 복잡한 래퍼를 만들 필요가 없습니다. 모든 것이 타입화되어 있고, API 자체가 TS 타입 추론을 최대한 활용하도록 설계되었습니다.
  • 더 이상 매직 스트링을 주입할 필요 없이, 함수를 import하고 호출하면 자동완성을 누릴 수 있습니다!
  • 동적으로 스토어를 추가할 필요가 없습니다. 모든 스토어가 기본적으로 동적이며, 이를 신경 쓸 필요도 없습니다. 물론 원한다면 수동으로 스토어를 등록할 수도 있지만, 자동이기 때문에 걱정할 필요가 없습니다.
  • _modules_의 중첩 구조가 사라졌습니다. 여전히 다른 스토어를 import하고 _사용_하여 암묵적으로 중첩할 수 있지만, Pinia는 기본적으로 평면 구조를 제공하며, 스토어 간의 교차 조합도 지원합니다. 스토어 간 순환 참조도 가능합니다.
  • _namespaced modules_이 없습니다. 스토어의 평면 아키텍처 덕분에, "네임스페이스"는 스토어 정의 방식에 내재되어 있으며, 모든 스토어가 네임스페이스화되어 있다고 볼 수 있습니다.

기존 Vuex ≤4 프로젝트를 Pinia로 변환하는 자세한 방법은 Vuex 마이그레이션 가이드를 참고하세요.

모두를 위한 문서 한글화