출시·고도화 중
Vue.js · Nuxt 안내서 · 4/6
화면이 커지면 상태를 어디에 둘지, 서버 데이터를 어떻게 불러올지가 중요해집니다. 이 장에서는 composable로 로직을 재사용하는 방법, Pinia로 전역 상태를 관리하는 방법, Nuxt의 useFetch·useAsyncData와 서버 라우트로 데이터를 주고받는 방법을 다룹니다.
| 상태의 범위 | 두는 곳 |
|---|---|
| 한 컴포넌트 안 | 컴포넌트의 ref · reactive |
| 부모와 자식 사이 | props와 emits, 또는 provide · inject |
| 여러 화면에서 공유 | Pinia 스토어 |
| 서버에서 온 데이터 | useFetch · useAsyncData(Nuxt), 또는 composable |
처음부터 모든 것을 전역 스토어에 넣기보다, 가장 가까운 곳에 두고 공유가 필요해질 때 위로 올리는 편이 관리하기 쉽습니다.
composable은 반응형 상태와 로직을 묶어 돌려주는 use로 시작하는 함수입니다. 여러 컴포넌트에서 같은 동작이 필요할 때 씁니다.
// composables/useCounter.ts
import { ref, computed } from "vue";
export function useCounter(initial = 0) {
const count = ref(initial);
const double = computed(() => count.value * 2);
const increment = () => count.value++;
const reset = () => (count.value = initial);
return { count, double, increment, reset };
}컴포넌트에서는 const { count, increment } = useCounter(10);처럼 쓰며, 호출할 때마다 독립된 상태가 만들어집니다. Nuxt에서는 composables/ 폴더의 함수가 자동으로 가져와집니다.
Pinia는 Vue 공식 상태 관리 라이브러리입니다. 스토어는 defineStore로 정의하며, composable과 같은 모양의 setup 문법을 쓸 수 있습니다. ref는 상태, computed는 getter, 함수는 action이 됩니다.
// stores/cart.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
type Item = { id: number; name: string; price: number; qty: number };
export const useCartStore = defineStore("cart", () => {
const items = ref<Item[]>([]);
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0));
function add(item: Omit<Item, "qty">) {
const found = items.value.find((i) => i.id === item.id);
if (found) found.qty++;
else items.value.push({ ...item, qty: 1 });
}
return { items, total, add };
});컴포넌트에서 스토어의 상태를 구조 분해하면 반응성이 끊기므로 storeToRefs를 씁니다. action은 그대로 구조 분해해도 됩니다.
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useCartStore } from "@/stores/cart";
const cart = useCartStore();
const { items, total } = storeToRefs(cart);
</script>
<template>
<p>{{ items.length }}개, 합계 {{ total }}원</p>
<button @click="cart.add({ id: 1, name: '책', price: 15000 })">담기</button>
</template>Nuxt에서는 npx nuxi module add pinia로 @pinia/nuxt 모듈을 추가하면 서버 렌더링 중 만든 상태가 클라이언트로 안전하게 넘어갑니다.
server/api/ 폴더의 파일은 API 엔드포인트가 됩니다. 파일 이름 끝에 .get, .post를 붙이면 HTTP 메서드를 제한할 수 있습니다.
// server/api/todos.get.ts
export default defineEventHandler(async (event) => {
const { done } = getQuery(event);
const todos = [
{ id: 1, text: "Vue 배우기", done: true },
{ id: 2, text: "Nuxt 배포하기", done: false },
];
return done === undefined ? todos : todos.filter((t) => String(t.done) === done);
});
// server/api/todos.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody<{ text: string }>(event);
if (!body?.text) {
throw createError({ statusCode: 400, statusMessage: "text가 필요합니다" });
}
return { id: Date.now(), text: body.text, done: false };
});반환한 객체는 JSON으로 응답됩니다. 실제 서비스에서는 이 자리에서 데이터베이스나 외부 API를 호출하며, 비밀 키는 useRuntimeConfig(event)로 읽습니다.
페이지에서 데이터를 불러올 때는 useFetch를 씁니다. 서버 렌더링 중에 한 번 불러온 결과를 페이지와 함께 보내므로 클라이언트에서 같은 요청을 다시 하지 않습니다.
<script setup lang="ts">
const { data: todos, status, error, refresh } = await useFetch("/api/todos");
async function addTodo(text: string) {
await $fetch("/api/todos", { method: "POST", body: { text } });
await refresh();
}
</script>
<template>
<p v-if="status === 'pending'">불러오는 중…</p>
<p v-else-if="error">오류: {{ error.message }}</p>
<ul v-else>
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
</ul>
</template>useFetch는 useAsyncData(키, () => $fetch(url))를 간단히 쓴 형태입니다. 여러 요청을 합치거나 데이터베이스 SDK처럼 $fetch가 아닌 함수를 부를 때는 useAsyncData를 직접 씁니다. 버튼 클릭처럼 사용자 동작에 따른 요청에는 $fetch를 씁니다. 서버와 클라이언트가 함께 쓰는 간단한 공유 상태는 useState("key", () => 초깃값)으로 만들 수 있습니다.
storeToRefs를 씁니다.server/api/ 파일은 defineEventHandler로 API를 만듭니다.useFetch·useAsyncData, 사용자 동작에 따른 요청은 $fetch로 처리합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.