Skip to content

Complete guide to

Mastering Pinia

written by its creator

State (상태)

상태는 대부분은, 스토어를 중심으로 이루어지며, 일반적으로 앱을 나타내는 상태를 정의하는 것으로 시작합니다. 피니아에서 상태는 초기 상태를 반환하는 함수로 정의됩니다. 이를 통해 피니아는 서버 측과 클라이언트 측 모두에서 작동할 수 있습니다.

js
import { defineStore } from 'pinia'

export const useStore = defineStore('storeId', {
  // 화살표 함수는 전체 유형 유추을 위해 권장됨. 
  state: () => {
    return {
      // 이 모든 속성은 자동으로 유형이 유추됨.
      count: 0,
      name: 'Eduardo',
      isAdmin: true,
      items: [],
      hasChanged: true,
    }
  },
})

TIP

Vue 2를 사용하는 경우, state에서 생성한 데이터는 Vue 인스턴스의 data와 동일한 규칙을 따릅니다. 즉, 상태 객체는 일반 객체여야 하며, 새 속성을 추가할 때 Vue.set()을 호출해야 합니다.

참조: Vue#data.

TypeScript

TS와 호환되는 상태를 만들기 위해 많은 작업을 수행할 필요가 없습니다: strict 또는 최소한 noImplicitThis가 활성화되어 있는지 확인하고 피니아가 자동으로 상태 유형을 추론합니다! 그러나 몇몇 경우에는 캐스팅으로 보조해야 합니다:

ts
export const useUserStore = defineStore('user', {
  state: () => {
    return {
      // 처음에 비어 있는 목록의 경우.
      userList: [] as UserInfo[],
      // 아직 로드되지 않은 데이터의 경우.
      user: null as UserInfo | null,
    }
  },
})

interface UserInfo {
  name: string
  age: number
}

원하는 경우, 인터페이스로 상태를 정의하고, state()로 반환 값 유형을 정의할 수 있습니다:

ts
interface State {
  userList: UserInfo[]
  user: UserInfo | null
}

export const useUserStore = defineStore('user', {
  state: (): State => {
    return {
      userList: [],
      user: null,
    }
  },
})

interface UserInfo {
  name: string
  age: number
}

state에 접근

기본적으로 store 인스턴스로 상태에 접근하여 상태를 직접 읽고 쓸 수 있습니다:

js
const store = useStore()

store.count++

만약 state()에 상태를 정의해 두지 않았다면, 새 상태 속성을 추가할 수 없습니다. 예를들어, state()secondCount가 정의되어 있지 않으면, state.secondCount = 2를 수행할 수 없습니다.

상태 재설정

Option Stores에서 스토어에서 $reset() 메서드를 호출하여 상태를 초기 값으로 _재설정_할 수 있습니다.

js
const store = useStore()

store.$reset()

내부적으로 이것은 state() 함수를 호출하여 새로운 상태 객체를 생성하고 현재 상태를 그것으로 대체합니다.

Setup Stores에서 자신만의 $reset() 메서드를 만들어야 합니다.

ts
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  function $reset() {
    count.value = 0
  }

  return { count, $reset }
})

옵션 API와 함께 사용

다음 예제는 아래와 같은 스토어가 생성되었다고 가정합니다:

js
// 예제 파일 경로:
// ./src/stores/counter.js

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
})

컴포지션 API를 사용하지 않고 computed, methods, ...를 사용하는 경우, mapState() 헬퍼를 사용하여 상태 속성을 읽기 전용 계산된 속성으로 매핑할 수 있습니다:

js
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  computed: {
    // 컴포넌트 내부에서 `this.count`로 접근할 수 있게 함.
    // `store.count`로 읽는 것과 동일.
    ...mapState(useCounterStore, ['counter']),
    // 위와 같지만 `this.myOwnName`으로 등록.
    ...mapState(useCounterStore, {
      myOwnName: 'count',
      // 스토어에 접근하는 함수를 작성할 수도 있음
      double: store => store.count * 2,
      // `this`에 접근할 수 있지만, 올바르게 입력되지 않음...
      magicValue(store) {
        return store.someGetter + this.count + this.double
      },
    }),
  },
}

수정 가능한 상태

이러한 상태 속성을 쓸 수 있도록 하려면(예: form이 있는 경우), mapWritableState()를 사용해야 합니다. mapState()처럼 함수를 전달할 수 없습니다:

js
import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  computed: {
    // 컴포넌트 내부에서 `this.count`로 접근할 수 있게 하고,
    // `this.count++`와 같이 수정도 허용함.
    // `store.count`에서 읽는 것과 동일.
    ...mapWritableState(useCounterStore, ['count']),
    // 위와 같지만 `this.myOwnName`으로 등록.
    ...mapWritableState(useCounterStore, {
      myOwnName: 'count',
    }),
  },
}

TIP

배열 전체를 cartItems = []처럼 바꾸지 않는 한, 배열 컬렉션은 mapWritableState()가 필요하지 않습니다. mapState()를 사용하면 컬렉션에서 메서드를 호출할 수 있습니다.

상태 변경하기

store.count++로 스토어를 직접 변경하는 방법 외에도, $patch 메소드를 호출할 수도 있습니다. 이것을 사용하여 state 객체의 일부분을 동시에 변경할 수 있습니다:

js
store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})

그러나 일부 mutations은 이러한 문법으로 적용하기가 정말 어렵거나 비용이 많이 듭니다. 컬렉션을 수정(예: 배열에서 요소를 푸시, 제거, 스플라이스)하려면, 새 컬렉션을 만들어야 합니다. 이 때문에 $patch 메소드는 패치 객체로 적용하기 어려운 이러한 종류의 mutations를 그룹화하는 함수도 허용합니다:

js
store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

여기서 주요 차이점은 $patch()를 사용하여 devtools에서 여러 변경 사항을 하나의 항목으로 그룹화할 수 있다는 것입니다. state$patch()에 대한 직접적인 변경 사항은 모두 devtools에 나타나며, 시간 추적이 가능합니다(아직 Vue 3에는 없음).

state 교체하기

반응성을 깨뜨릴 수 있으므로 스토어의 상태를 정확히 교체할 수 없습니다. 그러나 패치할 수 있습니다:

js
// 이것은 실제로 `$state`를 교체하지 않음.
store.$state = { count: 24 }
// 아래와 같이 내부적으로 `$patch()`를 호출함:
store.$patch({ count: 24 })

피니아 인스턴스의 state를 변경하여, 전체적으로 앱의 초기 상태를 설정할 수도 있습니다. 하이드레이션을 위한 SSR 동안 사용합니다.

js
pinia.state.value = {}

상태 구독하기

Vuex의 subscribe 메소드와 마찬가지로, 스토어의 $subscribe() 메소드를 통해 상태의 변경 사항을 감시할 수 있습니다. 일반 watch()보다 $subscribe() 사용시 장점은 구독이 여러 패치(예: 위에서 언급한 것처럼, $patch에 함수를 전달하고, 함수 내부에서 여러번의 패치가 실행됨) 이후에 한 번만 트리거된다는 것입니다.

js
cartStore.$subscribe((mutation, state) => {
  // import { MutationType } from 'pinia'
  mutation.type // 'direct' | 'patch object' | 'patch function'
  // `cartStore.$id`와 동일.
  mutation.storeId // 'cart'
  // `mutation.type === 'patch object'`에서만 사용 가능.
  mutation.payload // cartStore.$patch()에 전달된 패치 객체

  // 변경될 때마다 전체 상태를 로컬 스토리지에 유지
  localStorage.setItem('cart', JSON.stringify(state))
})

기본적으로 상태 구독은 컴포넌트에 추가된(스토어가 컴포넌트의 setup() 내부에 있는) 경우에 바인딩됩니다. 따라서 컴포넌트가 마운트 해제되면 자동으로 제거됩니다. 컴포넌트가 마운트 해제된 후에도 이를 유지하려면, 두 번째 인수로 현재 컴포넌트에서 상태 구독을 분리하는 { detached: true }를 전달합니다:

vue
<script setup>
const someStore = useSomeStore()

// 이 구독은 컴포넌트가 마운트 해제된 후에도 유지됨.
someStore.$subscribe(callback, { detached: true })
</script>

TIP

하나의 watch()를 사용하여 pinia 인스턴스의 전체 상태를 _감시_할 수 있습니다.

js
watch(
  pinia.state,
  (state) => {
    // 변경될 때마다 전체 상태를 로컬 스토리지에 유지
    localStorage.setItem('piniaState', JSON.stringify(state))
  },
  { deep: true }
)

Translated by pinia.vuejs.kr