Post

Pinia

Pinia에 대해서 적어봤습니다.

Pinia

주제 선정 이유

프론트엔드 개발을 진행하면서 Vue를 기반으로 화면을 구성하고 상태를 관리하는 데에는 점점 익숙해졌지만,
컴포넌트가 많아지고 상태가 여러 곳에서 공유되기 시작하면서 props를 통한 데이터 전달과 emit을 통한 이벤트 전달 방식으로는 구조가 점점 복잡해지는 문제를 경험하게 되었고, 특히 상태가 여러 컴포넌트에 걸쳐 흩어지면서 데이터의 흐름을 파악하기 어렵고 유지보수가 힘들어지는 상황을 겪게 되었습니다.

또한 부모 → 자식 → 손자 컴포넌트로 이어지는 props drilling 구조가 깊어질수록 불필요한 전달이 증가하고,
이벤트 흐름 역시 여러 단계를 거치면서 코드의 의도를 파악하기 어려워지는 문제를 느끼게 되었습니다.

그래서 이번 글에서는 Pinia를 사용할 때 자주 접하게 되는 Store 기반 상태 관리 구조를 중심으로, state / getter / action의 개념과 Composition API와의 연계 방식, 그리고 실제 코드에서 어떻게 활용되는지를 정리해보고자 합니다.

Pinia가 뭘까?

PiniaVue 3를 위한 공식 상태 관리 라이브러리로, 애플리케이션 전반에서 사용하는 상태를 중앙에서 관리할 수 있도록 도와주는 도구인데 기존에 propsemit을 통해 컴포넌트 간 데이터를 주고받는 방식은 구조가 단순할 때는
유용하지만, 컴포넌트가 많아지고 상태가 여러 곳에서 공유되기 시작하면 데이터 흐름이 복잡해지고 유지보수가
어려워지는 문제가 발생합니다.

이러한 문제를 해결하기 위해 Store라는 개념을 중심으로 상태를 한 곳에 정의하고, 필요한 컴포넌트에서 직접 가져와 사용할 수 있도록 해주며 필요한 상태를 직접 접근하고 수정할 수 있는 구조를 가지게 됩니다.

store - 상태 관리 단위

Pinia는 단순히 상태를 저장하는 도구가 아니라, 애플리케이션의 상태와 로직을 구조적으로 관리할 수 있도록 Store 단위를 중심으로 동작하고 이때 크게 state, getter, action이라는 세 가지 개념으로 구성됩니다.

state - 상태

state는 전역으로 관리할 데이터를 정의하는 영역으로, 여러 컴포넌트에서 공통으로 사용하는 값을 한 곳에 모아서
관리할 수 있는데 기존처럼 각 컴포넌트 내부에 상태를 따로 선언하는 것이 아니라, 공유가 필요한 데이터를 state
정의하고 필요한 곳에서 가져다 사용하는 방식으로 동작합니다.

이렇게 하면 컴포넌트 간에 props로 값을 전달하거나 emit으로 이벤트를 전달하지 않아도, 동일한 데이터를 여러
곳에서 직접 사용할 수 있어 구조가 훨씬 단순해지며 state는 반응형으로 동작하기 때문에 값이 변경되면 해당 값을
사용하는 컴포넌트가 자동으로 업데이트됩니다.

기존 방식

1
2
3
4
5
6
7
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const count = ref(0)
</script>
1
2
3
4
5
6
7
8
<!-- Child.vue -->
<script setup>
defineProps(['count'])
</script>

<template>
  <p>9</p>
</template>

Pinia 적용 후

1
2
3
4
5
6
7
8
// store/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  })
})
1
2
3
4
5
6
7
8
9
10
<!-- Child.vue -->
<script setup>
import { useCounterStore } from '@/store/counter'

const counter = useCounterStore()
</script>

<template>
  <p></p>
</template>

getter - 계산된 값

getterstate를 기반으로 새로운 값을 계산할 때 사용하는 기능으로, Vuecomputed와 유사한 역할을 하며
단순히 상태 값을 그대로 사용하는 것이 아니라, 특정 값을 가공하거나 여러 상태를 조합해 새로운 값을 만들어야 하는
경우에 활용되며, 의존하고 있는 state가 변경될 때만 다시 계산됩니다.

이를 통해 불필요한 연산을 줄일 수 있고, 화면에 필요한 데이터를 보다 효율적으로 관리할 수 있습니다.

기존 방식

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)

const double = computed(() => count.value * 2)
</script>

<template>
  <p></p>
</template>

Pinia 적용 후

1
2
3
4
5
6
7
8
9
10
11
// store/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    double: (state) => state.count * 2
  }
})
1
2
3
4
5
6
7
8
9
<script setup>
import { useCounterStore } from '@/store/counter'

const counter = useCounterStore()
</script>

<template>
  <p></p>
</template>

action - 상태 변경 로직

actionstate를 변경하는 로직을 정의하는 영역으로, 단순한 값 변경뿐만 아니라 비동기 처리까지 함께 수행할 수 있는데 기존에는 상태를 변경하는 로직이 여러 컴포넌트에 분산될 수 있었지만 Pinia에서는 이를 action으로 모아서 관리함으로써 상태 변화의 흐름을 명확하게 만들 수 있습니다.

또한 동일한 로직을 여러 컴포넌트에서 재사용할 수 있기 때문에 코드 중복을 줄이고 유지보수성을 높이는데에도 도움이 됩니다.

기존 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { ref } from 'vue'

const count = ref(0)

const increment = () => {
  count.value++
}
</script>

<template>
  <button @click="increment">+</button>
</template>

Pinia 적용 후

1
2
3
4
5
6
7
8
9
10
11
12
13
// store/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})
1
2
3
4
5
6
7
8
9
<script setup>
import { useCounterStore } from '@/store/counter'

const counter = useCounterStore()
</script>

<template>
  <button @click="counter.increment">+</button>
</template>

그래서 어떻게 쓰라고?

실제로는 모든 기능을 한 번에 사용하는 것이 아니라 상황에 맞게 state, getter, action을 선택해서 사용하는 것이 중요한데 초기에는 단순한 전역 상태를 state로 관리하는 것부터 시작하고, 상태를 기반으로 값을 가공해야 하는
경우에는 getter를 활용하며, 상태 변경 로직이 복잡해지거나 여러 곳에서 재사용이 필요한 경우에는 action으로
분리하는 방식으로 확장해 나갈 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    double: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { useCounterStore } from '@/store/counter'

const counter = useCounterStore()

// 상태 사용
console.log(counter.count)

// getter 사용
console.log(counter.double)

// action 실행
counter.increment()
</script>

이처럼 Pinia는 단순히 상태를 저장하는 것을 넘어, 상태와 로직을 하나의 Store로 묶어 관리하면서 필요한
컴포넌트에서 직접 가져와 사용하는 방식으로 동작하며 stategetteraction으로 이어지는 구조를 통해
데이터의 흐름을 명확하게 구성할 수 있습니다.

결과적으로 전역 상태를 보다 직관적으로 관리하고, 코드의 복잡도를 줄이면서 확장 가능한 구조를 만들어가는 방식으로 사용하는 것이 중요하다고 볼 수 있습니다.

마무리

Pinia를 정리하면서 느낀 핵심을 다시 정리해보면 다음과 같습니다.

  1. Pinia는 전역 상태를 중앙에서 관리함으로써 컴포넌트 간 데이터 전달 구조를 단순화해주는 상태 관리 라이브러리입니다.
  2. state, getter, action을 통해 상태, 계산, 로직을 명확하게 분리해 보다 구조적인 코드 설계를 가능하게 합니다.
  3. propsemit 중심의 복잡한 데이터 흐름을 줄이고, 필요한 상태를 직접 가져와 사용하는 방식으로 가독성과
    유지보수성을 향상시킬 수 있습니다.
  4. Pinia는 단순한 상태 관리 도구를 넘어, 상태와 로직을 Store 단위로 구성하여 애플리케이션의 구조를 명확하게
    만들어주는 역할을 합니다.
This post is licensed under CC BY 4.0 by the author.