이 장에서는 Vue 코드를 읽고 쓰는 데 필요한 핵심 개념을 다룹니다. 단일 파일 컴포넌트, 반응성 API, 템플릿 디렉티브, props와 emits, 그리고 Nuxt의 파일 기반 라우팅과 자동 가져오기입니다.
단일 파일 컴포넌트와 Composition API
.vue 파일은 <script setup>, <template>, <style> 세 블록으로 이루어집니다. <script setup>은 Composition API를 간결하게 쓰는 문법으로, 그 안에서 선언한 변수·함수·가져온 컴포넌트는 템플릿에서 곧바로 쓸 수 있습니다. scoped를 붙인 스타일은 이 컴포넌트에만 적용됩니다.
반응성: ref, reactive, computed, watch
ref는 어떤 값이든 반응형으로 감싸며, 스크립트에서는 .value로 읽고 씁니다. 템플릿에서는 .value 없이 씁니다. reactive는 객체 자체를 반응형으로 만듭니다. computed는 다른 상태에서 계산된 값을 캐시하고, watch는 값이 바뀔 때 부수 효과를 실행합니다.
<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue";
const query = ref("");
const filter = reactive({ onlyDone: false });
const todos = ref([
{ id: 1, text: "Vue 배우기", done: true },
{ id: 2, text: "Nuxt 써 보기", done: false },
]);
const visible = computed(() =>
todos.value
.filter((t) => !filter.onlyDone || t.done)
.filter((t) => t.text.includes(query.value)),
);
watch(query, (next, prev) => {
console.log(`검색어: ${prev} → ${next}`);
});
</script>
<template>
<input v-model="query" placeholder="검색" />
<label><input type="checkbox" v-model="filter.onlyDone" /> 완료만</label>
<ul>
<li v-for="todo in visible" :key="todo.id">{{ todo.text }}</li>
</ul>
<p v-if="visible.length === 0">결과가 없습니다.</p>
</template>
computed는 순수한 계산에만 쓰고, API 호출이나 로그처럼 바깥에 영향을 주는 일은 watch나 watchEffect에 맡깁니다.
템플릿 디렉티브
컴포넌트, props, emits
부모는 props로 데이터를 내려 주고, 자식은 이벤트(emits)로 변경을 알립니다. TypeScript에서는 defineProps와 defineEmits에 타입을 바로 적습니다.
<!-- components/TodoItem.vue -->
<script setup lang="ts">
const props = defineProps<{ text: string; done?: boolean }>();
const emit = defineEmits<{ toggle: []; remove: [reason: string] }>();
</script>
<template>
<li :class="{ done: props.done }">
<input type="checkbox" :checked="props.done" @change="emit('toggle')" />
{{ props.text }}
<button @click="emit('remove', 'user')">삭제</button>
</li>
</template>
부모에서는 <TodoItem :text="todo.text" :done="todo.done" @toggle="todo.done = !todo.done" />처럼 씁니다. props는 읽기 전용이므로 자식이 직접 바꾸지 않고 이벤트로 요청합니다. 양방향 바인딩이 필요하면 defineModel()을 쓰면 부모의 v-model과 바로 연결됩니다. 자식 사이에 내용을 끼워 넣을 때는 <slot />을 씁니다.
Nuxt의 파일 기반 라우팅
Nuxt에서는 pages/ 폴더의 파일 경로가 URL이 됩니다. 대괄호 이름은 동적 매개변수입니다.
pages/index.vue → /
pages/about.vue → /about
pages/posts/[id].vue → /posts/:id
<!-- pages/posts/[id].vue -->
<script setup lang="ts">
const route = useRoute();
definePageMeta({ layout: "default" });
</script>
<template>
<h1>글 {{ route.params.id }}</h1>
<NuxtLink to="/">목록으로</NuxtLink>
</template>
app.vue에 <NuxtPage />를 두면 현재 URL에 맞는 페이지가 그 자리에 렌더링되고, <NuxtLink>는 페이지를 새로 불러오지 않고 이동합니다.
자동 가져오기
위 예제에 import 문이 없다는 점에 주목하세요. Nuxt는 ref, computed 같은 Vue API, useRoute·useFetch 같은 Nuxt 컴포저블, components/의 컴포넌트, composables/와 utils/의 함수를 자동으로 가져옵니다. components/base/Button.vue는 폴더 이름이 앞에 붙어 <BaseButton>이 됩니다. 명시적으로 쓰고 싶다면 import { ref } from "#imports"처럼 가져올 수도 있습니다.
정리
<script setup>에서 선언한 것은 템플릿에서 바로 쓰입니다.ref는 .value로, reactive는 객체 그대로 다루며, 파생 값은 computed, 부수 효과는 watch에 둡니다.- 데이터는 props로 내려가고 변경은 emits로 올라옵니다.
- Nuxt는
pages/ 파일 경로로 라우트를 만들고, API·컴포넌트·composable을 자동으로 가져옵니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.