학습일지/프론트엔드
[Vue.js] 공식문서 예제 살펴보기
zinyy
2024. 8. 24. 02:54
인턴을 시작하면서 평소에 사용하던 React가 아닌, 해당 회사에 사용하는 Vue.js를 새로 공부하게 되었어요!
비록 공부할 수 있는 기간이 짧아 3일 정도로 그쳤지만 공식문서의 예제 위주로 공부했던 내용과
Vue.js의 핵심만 정리해보려고 합니다 :-)
Vue.js
Vue.js - The Progressive JavaScript Framework
vuejs.org
Vue.js의 공식 문서를 바탕으로 한 내용입니다 !
모든 예제를 다 살펴보지는 않고 Vue.js의 주요 내용을 알아볼 수 있을만한 예제 위주로 작성할 예정이에요.
Hello World
<script setup>
import { ref } from 'vue'
const message = ref('Hello World!')
</script>
<template>
<h1>{{ message }}</h1>
</template>
- ref : Vue.js에서 'ref()' 는 데이터가 반응형이 되도록 하는 기능입니다. React에서 'useState'를 사용하는 것과 유사합니다.
- 'message' 변수는 'ref'로 감싸져 있어 값이 변경될 때마다 UI가 자동으로 업데이트됩니다.
- 템플릿에서 '{{ message }}' 와 같은 이중 중괄호 문법을 사용해 'ref' 로 감싼 변수를 접근할 수 있습니다.
사용자 입력 핸들링
<script setup>
import { ref } from 'vue'
const message = ref('Hello World!')
function reverseMessage() {
message.value = message.value.split('').reverse().join('')
}
function notify() {
alert('navigation was prevented.')
}
</script>
<template>
<h1>{{ message }}</h1>
<button @click="reverseMessage">Reverse Message</button>
<button @click="message += '!'">Append "!"</button>
<a href="https://vuejs.org" @click.prevent="notify">
A link with e.preventDefault()
</a>
</template>
<style>
button, a {
display: block;
margin-bottom: 1em;
}
</style>
- '@click' : Vue.js에서 이벤트 핸들링을 위한 문법입니다. React의 'onClick'과 유사하며, 'v-on:click=""' 의 단축형으로 사용됩니다.
- '.value' : 'ref'로 감싸진 변수에 접근하거나 변경할 때 사용합니다. 템플릿 내부에서는 '.value'를 생략할 수 있지만, script에서는 꼭 '.value'로 접근해야 합니다.
- '@click.prevent' : 클릭 시 기본 동작을 막는 역할을 합니다. 위 예제에서는 'a' 태그의 기본 동작을 막고, 'notify' 함수를 실행합니다.
속성 바인딩
<script setup>
import { ref } from 'vue'
const message = ref('Hello World!')
const isRed = ref(true)
const color = ref('green')
function toggleRed() {
isRed.value = !isRed.value
}
function toggleColor() {
color.value = color.value === 'green' ? 'blue' : 'green'
}
</script>
<template>
<p>
<span :title="message">
Hover your mouse over me for a few seconds to see my dynamically bound title!
</span>
</p>
<p :class="{ red: isRed }" @click="toggleRed">
This should be red... but click me to toggle it.
</p>
<p :style="{ color }" @click="toggleColor">
This should be green, and should toggle between green and blue on click.
</p>
</template>
<style>
.red {
color: red;
}
</style>
- 'v-bind ( : )' : Vue.js에서 속성 값을 동적으로 할당할 때 사용합니다. 예를 들어 ':title="message"'는 'message'의 값을 'title' 속성에 바인딩합니다.
- ':class' : 객체 문법을 사용해 조건에 따라 클래스를 적용합니다. 예제에서는 'isRed'가 'true'일 때 'red' 클래스가 적용됩니다.
- ':style' : 인라인 스타일을 동적으로 바인딩할 수 있으며, 이 예제에서는 'color' 변수를 'p' 태그의 'color' 스타일 속성에 바인딩했습니다.
조건문과 반복문
<script setup>
import { ref } from 'vue'
const show = ref(true)
const list = ref([1, 2, 3])
</script>
<template>
<button @click="show = !show">Toggle List</button>
<button @click="list.push(list.length + 1)">Push Number</button>
<button @click="list.pop()">Pop Number</button>
<button @click="list.reverse()">Reverse List</button>
<ul v-if="show && list.length">
<li v-for="(item, index) of list" :key="index">{{ item }}</li>
</ul>
<p v-else-if="list.length">List is not empty, but hidden.</p>
<p v-else>List is empty.</p>
</template>
- 'v-for' : 배열을 리스트로 렌더링 할 때 사용합니다. 예제에서는 'list' 배열의 각 요소를 '<li>' 태그로 반복 렌더링해 목록을 화면에 표시합니다. ( React에서는 'map()' 함수를 사용하여 리스트를 반복 렌더링 )
- 'key' : 'v-for'로 반복 렌더링할 때, 고유한 'key' 값을 설정해 주는 것이 중요합니다. 위 예제에서는 'index'를 'key'로 사용했습니다.
- 'v-if', 'v-else-if', 'v-else' : 조건부 렌더링을 위해 사용합니다. 'show'와 'list.length'를 조건으로 'ul' 태그의 렌더링을 제어합니다.
공식문서의 예제에는 key 값을 따로 할당해주지 않았지만 제가 직접 vue.js 프로젝트를 만들어서 실행했을 때는 오류가 나더라구요.
그래서 list를 반복할 때 index도 함께 받아와서 key 값에 바인딩해 주었습니다!
폼 바인딩
Vue.js에서는 'v-model' 디렉티브를 사용하여 폼 요소와 데이터 간의 양방향 바인딩을 쉽게 구현할 수 있습니다.
이를 통해 사용자의 입력 값과 Vue 컴포넌트의 상태를 자동으로 동기화할 수 있습니다.
'v-model'은 텍스트 입력, 체크박스, 라디오 버튼, 셀렉트 박스 등 다양한 폼 요소에서 사용할 수 있습니다.
<script setup>
import { ref } from 'vue';
const text = ref('Edit me');
const checked = ref(true);
const checkedNames = ref(['Jack']);
const picked = ref('One');
const selected = ref('A');
const multiSelected = ref(['A']);
</script>
<template>
<!-- v-model : 양방향 바인딩 -->
<!-- 사용자 입력과 Vue 컴포넌트 상태 연결 -->
<h2>Text Input</h2>
<input v-model="text" />
<p>{{ text }}</p>
<h2>Checkbox</h2>
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">Checked: {{ checked }}</label>
<h2>Multi Checkbox</h2>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>
<p>Checked names: {{ checkedNames }}</p>
<h2>Radio</h2>
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<br />
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
<p>Picked: {{ picked }}</p>
<h2>Select</h2>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<p>Selected: {{ selected }}</p>
<h2>Multi Select</h2>
<select v-model="multiSelected" multiple style="width: 100px">
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<p>Selected: {{ multiSelected }}</p>
</template>
- 'v-model' : 폼 요소와 Vue 컴포넌트 상태를 양방향으로 바인딩합니다. 예를 들어 텍스트 입력 ('input') 에 'v-model="text"'를 사용하면, 사용자가 입력한 텍스트가 자동으로 'text' 변수에 반영되고, 반대로 'text' 변수의 값이 바뀌면 입력 필드에 즉시 반영됩니다. ( React에서는 'useState'와 'onChange' 핸들러를 이용해 비슷한 기능을 수동으로 구현 )
- 체크박스 : 단일 체크박스는 'v-model'을 사용해 'true' 또는 'false' 값을 바인딩할 수 있습니다. 다중 체크박스를 선택된 값들을 배열 형태로 관리할 수 있습니다.
- 라디오 버튼 : 여러 라디오 버튼 중 하나를 선택하면, 해당 값이 바인딩된 변수에 저장됩니다.
- 셀렉트 박스 : 'v-model'을 사용해 단일 또는 다중 선택 박스의 값을 바인딩할 수 있습니다.
단순한 컴포넌트
<script setup>
import { ref } from 'vue';
import TodoItem from './TodoItem.vue';
const groceryList = ref([
{ id: 1, text: 'Vegetables' },
{ id: 2, text: 'Cheese' },
{ id: 3, text: 'Whatever else humans are supposed to eat' },
]);
</script>
<template>
<ol>
<TodoItem
v-for="item in groceryList"
:todo="item"
:key="item.id"
></TodoItem>
</ol>
</template>
<style>
.red {
color: red;
}
</style>
- 컴포넌트 사용 : 'TodoItem'이라는 자식 컴포넌트를 사용하여 각각의 리스트 항목을 렌더링 합니다.
- Props 전달 : ':todo="item"' 구문을 통해 부모 컴포넌트에서 자식 컴포넌트로 'item' 객체를 'props'로 전달합니다. 이는 컴포넌트 간 데이터 전달의 기본적인 방법입니다.
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
todo: Object,
});
</script>
<template>
<li>{{ props.todo.text }}</li>
</template>
- 'defineProps' : 'defineProps' 는 Vue.js의 Composition API에서 사용되는 함수로, 부모 컴포넌트로부터 전달된 'props'를 정의합니다. 이 예제에서는 'todo'라는 객체를 'props'로 받아옵니다.
- 템플릿에서 데이터 사용 : '{{ props.todo.text }}' 를 사용하여 'todo' 객체의 'text' 값을 '<li>' 요소에 렌더링 합니다. 이로 인해 'groceryList'의 각 항목이 리스트의 아이템으로 출력됩니다.
Markdown 편집기
<script setup>
import { marked } from 'marked';
import { debounce } from 'lodash-es';
import { ref, computed } from 'vue';
const input = ref('# hello');
const output = computed(() => marked(input.value));
const update = debounce((e) => {
input.value = e.target.value;
}, 100);
</script>
<template>
<div class="editor">
<textarea class="input" :value="input" @input="update"></textarea>
<div class="output" v-html="output"></div>
</div>
</template>
<style>
body {
margin: 0;
}
.editor {
height: 100vh;
display: flex;
}
.input,
.output {
overflow: auto;
width: 50%;
height: 100%;
box-sizing: border-box;
padding: 0 20px;
}
.input {
border: none;
border-right: 1px solid #ccc;
resize: none;
outline: none;
background-color: #f6f6f6;
font-size: 14px;
font-family: 'Monaco', courier, monospace;
padding: 20px;
}
code {
color: #f66;
}
</style>
- computed : Vue.js에서 종속된 데이터가 변경될 때마다 특정 값을 다시 계산하는 데 사용됩니다. 여기서는 'input' 데이터가 변경될 때마다 Markdown 텍스트를 HTML로 변환하여 output에 저장합니다.
- debounce : 'debounce'는 'lodash-es' 라이브러리에서 제공하는 함수로, 특정 함수가 호출된 후 일정 시간 동안 추가 호출이 발생하지 않을 때까지 지연시킵니다. 이 예제에서는 사용자가 입력한 내용이 너무 빠르게 반영되지 않도록 입력 처리를 지연시켜 성능을 최적화합니다.
- 'v-html' : 'v-html' 디렉티브를 사용하여 'output' 변수에 저장된 HTML을 DOM에 직접 렌더링 합니다.
데이터 가져오기
<script setup>
import { ref, watchEffect } from 'vue';
const API_URL = `https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=`;
const branches = ['main', 'v2-compat'];
const currentBranch = ref(branches[0]);
const commits = ref(null);
watchEffect(async () => {
const url = `${API_URL}${currentBranch.value}`;
commits.value = await (await fetch(url)).json();
});
function truncate(v) {
const newline = v.indexOf('\n');
return newline > 0 ? v.slice(0, newline) : v;
}
function formatDate(v) {
return v.replace(/T|Z/g, ' ');
}
</script>
<template>
<h1>Latest Vue Core Commits</h1>
<template v-for="branch in branches" :key="branch">
<input
type="radio"
:id="branch"
:value="branch"
name="branch"
v-model="currentBranch"
/>
<label :for="branch">{{ branch }}</label>
</template>
<p>vuejs/vue@{{ currentBranch }}</p>
<ul>
<li v-for="{ html_url, sha, author, commit } in commits" :key="sha">
<a :href="html_url" target="_blank" class="commit">{{
sha.slice(0, 7)
}}</a>
- <span class="message">{{ truncate(commit.message) }}</span
><br />
by
<span class="author">
<a :href="author.html_url" target="_blank">{{ commit.author.name }}</a>
</span>
at <span class="date">{{ formatDate(commit.author.date) }}</span>
</li>
</ul>
</template>
<style>
a {
text-decoration: none;
color: #42b883;
}
li {
line-height: 1.5em;
margin-bottom: 20px;
}
.author,
.date {
font-weight: bold;
}
</style>
- 'watchEffect' : Vue3에서 새로 도입된 기능으로, 반응형 데이터가 변경될 때마다 주어진 콜백 함수를 실행합니다. 이 예제에서는 'currentBranch'가 변경될 때마다 GitHub API를 호출하여 최신 커밋 데이터를 가져옵니다.
- 리액트에서는 이와 같은 작업을 위해 'useEffect'를 사용합니다. 하지만 Vue의 'watchEffect'는 반응형 데이터의 변경을 자동으로 추적하여, 필요할 때만 API 호출을 트리거한다는 점에서 매우 직관적입니다.
트리 뷰
<script setup>
import { ref, computed, defineProps } from 'vue';
const props = defineProps({
model: Object,
});
const localModel = ref(JSON.parse(JSON.stringify(props.model))); // Props의 사본 생성
const isOpen = ref(false);
const isFolder = computed(() => {
return localModel.value.children && localModel.value.children.length;
});
function toggle() {
isOpen.value = !isOpen.value;
}
function changeType() {
if (!isFolder.value) {
localModel.value.children = [];
addChild();
isOpen.value = true;
}
}
function addChild() {
localModel.value.children.push({ name: 'new stuff' });
}
</script>
<template>
<li>
<div :class="{ bold: isFolder }" @click="toggle" @dblclick="changeType">
{{ model.name }}
<span v-if="isFolder">[{{ isOpen ? '-' : '+' }}]</span>
</div>
<ul v-show="isOpen" v-if="isFolder">
<TreeItem
class="item"
v-for="(model, index) in model.children"
:model="model"
:key="index"
>
</TreeItem>
<li class="add" @click="addChild">+</li>
</ul>
</li>
</template>
- 'props'로 부모 컴포넌트로부터 'model' 객체를 받아옵니다.
기존 예제에서는 props를 직접 수정하도록 되어 있었으나, Vue에서는 props의 불변성을 유지해야 하므로, 'model'의 사본을 'localModel' 이라는 'ref'로 생성하여, 로컬 상태에서 데이터를 수정할 수 있도록 했습니다!
이를 통해 props의 원본 데이터는 그대로 유지되면서, 자식 컴포넌트에서의 데이터 변경이 가능하도록 했습니다.
SVG 그래프
<script setup>
import PolyGraph from './PolyGraph.vue';
import { ref, reactive } from 'vue';
const newLabel = ref('');
const stats = reactive([
{ label: 'A', value: 100 },
{ label: 'B', value: 100 },
{ label: 'C', value: 100 },
{ label: 'D', value: 100 },
{ label: 'E', value: 100 },
{ label: 'F', value: 100 },
]);
function add(e) {
e.preventDefault();
if (!newLabel.value) return;
stats.push({
label: newLabel.value,
value: 100,
});
newLabel.value = '';
}
function remove(stat) {
if (stats.length > 3) {
stats.splice(stats.indexOf(stat), 1);
} else {
alert("Can't delete more!");
}
}
</script>
<template>
<svg width="200" height="200">
<PolyGraph :stats="stats"></PolyGraph>
</svg>
<div v-for="(stat, index) in stats" :key="index">
<label>{{ stat.label }}</label>
<input type="range" v-model="stat.value" min="0" max="100" />
<span>{{ stat.value }}</span>
<button @click="remove(stat)" class="remove">X</button>
</div>
<form id="add">
<input name="newlabel" v-model="newLabel" />
<button @click="add">Add a Stat</button>
</form>
<pre id="raw">{{ stats }}</pre>
</template>
<style>
polygon {
fill: #42b983;
opacity: 0.75;
}
circle {
fill: transparent;
stroke: #999;
}
text {
font-size: 10px;
fill: #666;
}
label {
display: inline-block;
margin-left: 10px;
width: 20px;
}
#raw {
position: absolute;
top: 0;
left: 300px;
}
</style>
- 'ref'와 'reactive' : 'reactive'는 'ref'와 똑같이 반응형 데이터를 만드는 함수이나, 'ref'는 단일 값을 반응형으로 만드는 함수이고 'reactive'는 객체나 배열과 같은 복합 데이터 구조를 반응형으로 만드는 함수입니다.
트랜지션으로 모달 구현
<script setup>
import Modal from './AppModal.vue';
import { ref } from 'vue';
const showModal = ref(false);
</script>
<template>
<button id="show-modal" @click="showModal = true">Show Modal</button>
<Teleport to="body">
<modal :show="showModal" @close="showModal = false">
<template #header>
<h3>Custom Header</h3>
</template>
</modal>
</Teleport>
</template>
- 'Teleport' : Vue3에서 새로 도입된 기능으로, 특정 컴포넌트의 렌더링 위치를 DOM의 다른 부분으로 이동시킬 수 있습니다. 이 예제에서 모달 창은 'body' 태그 내부로 텔레포트되어 전체 화면에서 쉽게 접근할 수 있게 됩니다.
- 리액트에는 'ReactDOM.createPortal'이 비슷한 역할을 합니다.
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
show: Boolean,
});
</script>
<template>
<Transition name="app-modal">
<div v-if="props.show" class="app-modal-mask">
<div class="app-modal-container">
<div class="app-modal-header">
<slot name="header">default header</slot>
</div>
<div class="app-modal-body">
<slot name="body">default body</slot>
</div>
<div class="app-modal-footer">
<slot name="footer">
default footer
<button class="app-modal-default-button" @click="$emit('close')">
OK
</button>
</slot>
</div>
</div>
</div>
</Transition>
</template>
<style>
.app-modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
transition: opacity 0.3s ease;
}
.app-modal-container {
width: 300px;
margin: auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.app-modal-header h3 {
margin-top: 0;
color: #42b983;
}
.app-modal-body {
margin: 20px 0;
}
.app-modal-default-button {
float: right;
}
.app-modal-enter-from {
opacity: 0;
}
.app-modal-leave-to {
opacity: 0;
}
.app-modal-enter-from .app-modal-container,
.app-modal-leave-to .app-modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>
- 'Transition' : 모달 창이 나타나고 사라질 때의 애니메이션 효과를 적용하는 데 사용됩니다. Vue.js에서 'Transition' 컴포넌트는 특정 클래스('app-modal-enter-from', 'app-modal-leave-to')를 통해 트랜지션 효과를 관리합니다.
- 리액트에서는 CSS 애니메이션이나 'React Transition Group' 라이브러리를 사용해 유사한 효과를 구현할 수 있습니다.
- 'slot' : 부모 컴포넌트로부터 전달받은 콘텐츠를 렌더링 합니다. 이 예제에서 'header', 'body', 'footer' 슬롯을 사용해 모달의 각 부분을 커스터마이징 할 수 있습니다.
- 리액트의 'children'과 비슷한 개념으로, 부모 컴포넌트에서 자식 컴포넌트로 콘텐츠를 전달할 수 있게 합니다.
- 'emit' : 이 예제에서는 '@click="$emit('close')'이 사용되어 부모 컴포넌트로 'close' 이벤트를 전달합니다. 이 이벤트를 통해 모달 창이 닫히도록 상태가 업데이트됩니다.
- 리액트에서는 이벤트 핸들러와 'props'를 통해 자식 컴포넌트가 부모 컴포넌트의 상태를 업데이트할 수 있습니다.
트랜지션으로 리스트 구현
<script setup>
import { shuffle as _shuffle } from 'lodash-es';
import { ref } from 'vue';
const getInitialItems = () => [1, 2, 3, 4, 5];
const items = ref(getInitialItems());
let id = items.value.length + 1;
function insert() {
const i = Math.round(Math.random() * items.value.length);
items.value.splice(i, 0, id++);
}
function reset() {
items.value = getInitialItems();
id = items.value.length + 1;
}
function shuffle() {
items.value = _shuffle(items.value);
}
function remove(item) {
const i = items.value.indexOf(item);
if (i > -1) {
items.value.splice(i, 1);
}
}
</script>
<template>
<button @click="insert">Insert at random index</button>
<button @click="reset">Reset</button>
<button @click="shuffle">Shuffle</button>
<TransitionGroup tag="ul" name="fade" class="container">
<li v-for="item in items" class="item" :key="item">
{{ item }}
<button @click="remove(item)">x</button>
</li>
</TransitionGroup>
</template>
<style>
.container {
position: relative;
padding: 0;
list-style-type: none;
}
.item {
width: 100%;
height: 30px;
background-color: #f3f3f3;
border: 1px solid #666;
box-sizing: border-box;
}
/* 1. declare transition */
.fade-move,
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}
/* 2. declare enter from and leave to state */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scaleY(0.01) translate(30px, 0);
}
/* 3. ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.fade-leave-active {
position: absolute;
}
</style>
- 'shuffle' : 'lodash' 라이브러리의 'shuffle' 메서드를 사용해 리스트를 무작위로 섞습니다.
- 'TransitionGroup' : 리스트 아이템의 삽입과 삭제 시 애니메이션 효과를 적용합니다. Vue의 'TransitionGroup'은 리스트 내에서 요소가 추가되거나 제거될 때 트랜지션 효과를 쉽게 적용할 수 있게 해 줍니다. 이 예제에서는 'fade'라는 이름의 트랜지션이 적용되었으며, 'opacity'와 'transform' 속성을 이용해 요소의 추가와 제거 시 애니메이션을 적용했습니다.
- 리액트에서는 'React Transition Group' 라이브러리를 사용하여 유사한 트랜지션 효과를 적용할 수 있습니다. 하지만 리액트에서는 이러한 트랜지션을 직접 관리해야 하며, Vue처럼 내장된 트랜지션 시스템이 있지는 않습니다.
타이머
<script setup>
import { ref, onUnmounted, computed } from 'vue';
const duration = ref(15 * 1000);
const elapsed = ref(0);
let lastTime;
let handle;
const update = () => {
elapsed.value = performance.now() - lastTime;
if (elapsed.value >= duration.value) {
cancelAnimationFrame(handle);
} else {
handle = requestAnimationFrame(update);
}
};
const reset = () => {
elapsed.value = 0;
lastTime = performance.now();
update();
};
const progressRate = computed(() =>
Math.min(elapsed.value / duration.value, 1)
);
reset();
onUnmounted(() => {
cancelAnimationFrame(handle);
});
</script>
<template>
<label>Elapsed Time: <progress :value="progressRate"></progress></label>
<div>{{ (elapsed / 1000).toFixed(1) }}s</div>
<div>
Duration: <input type="range" v-model="duration" min="1" max="30000" />
{{ (duration / 1000).toFixed(1) }}s
</div>
<button @click="reset">Reset</button>
</template>
<style>
.elapsed-container {
width: 300px;
}
.elapsed-bar {
background-color: red;
height: 10px;
}
</style>
- 'onUnmounted' : 'onUnmounted' 훅을 사용하여 컴포넌트가 제거될 때 실행될 로직을 정의합니다. 여기서는 타이머를 중단하기 위해 'cancelAnimationFrame(handle)'이 호출됩니다.
- 리액트에서는 'useEffect'를 사용하여 컴포넌트가 언마운트될 때 실행할 코드를 'return'문으로 정의할 수 있습니다.
CRUD
<script setup>
import { ref, reactive, computed, watch } from 'vue'
const names = reactive(['Emil, Hans', 'Mustermann, Max', 'Tisch, Roman'])
const selected = ref('')
const prefix = ref('')
const first = ref('')
const last = ref('')
const filteredNames = computed(() =>
names.filter((n) =>
n.toLowerCase().startsWith(prefix.value.toLowerCase())
)
)
watch(selected, (name) => {
;[last.value, first.value] = name.split(', ')
})
function create() {
if (hasValidInput()) {
const fullName = `${last.value}, ${first.value}`
if (!names.includes(fullName)) {
names.push(fullName)
first.value = last.value = ''
}
}
}
function update() {
if (hasValidInput() && selected.value) {
const i = names.indexOf(selected.value)
names[i] = selected.value = `${last.value}, ${first.value}`
}
}
function del() {
if (selected.value) {
const i = names.indexOf(selected.value)
names.splice(i, 1)
selected.value = first.value = last.value = ''
}
}
function hasValidInput() {
return first.value.trim() && last.value.trim()
}
</script>
<template>
<div><input v-model="prefix" placeholder="Filter prefix"></div>
<select size="5" v-model="selected">
<option v-for="name in filteredNames" :key="name">{{ name }}</option>
</select>
<label>Name: <input v-model="first"></label>
<label>Surname: <input v-model="last"></label>
<div class="buttons">
<button @click="create">Create</button>
<button @click="update">Update</button>
<button @click="del">Delete</button>
</div>
</template>
<style>
* {
font-size: inherit;
}
input {
display: block;
margin-bottom: 10px;
}
select {
float: left;
margin: 0 1em 1em 0;
width: 14em;
}
.buttons {
clear: both;
}
button + button {
margin-left: 5px;
}
</style>
- 'watch' : 'watchEffect'와 비슷하게 반응형 상태를 감시하고, 그 상태가 변경될 때 특정 작업을 수행하기 위해 사용됩니다. 하지만 'watch'는 특정 반응형 상태를 감시하도록 설정하여 전달된 상태만 감시되며 기본적으로 상태가 변경되기 전까지는 실행되지 않는다는 차이점을 가지고 있습니다.
원 그리기
<script setup>
import { ref, shallowReactive, toRaw } from 'vue';
const history = shallowReactive([[]]);
const index = ref(0);
const circles = ref([]);
const selected = ref();
const adjusting = ref(false);
function onClick({ clientX: x, clientY: y }) {
if (adjusting.value) {
adjusting.value = false;
selected.value = null;
push();
return;
}
selected.value = [...circles.value].reverse().find(({ cx, cy, r }) => {
const dx = cx - x;
const dy = cy - y;
return Math.sqrt(dx * dx + dy * dy) <= r;
});
if (!selected.value) {
circles.value.push({
cx: x,
cy: y,
r: 50,
});
push();
}
}
function adjust(circle) {
selected.value = circle;
adjusting.value = true;
}
function push() {
history.length = ++index.value;
history.push(clone(circles.value));
console.log(toRaw(history));
}
function undo() {
circles.value = clone(history[--index.value]);
}
function redo() {
circles.value = clone(history[++index.value]);
}
function clone(circles) {
return circles.map((c) => ({ ...c }));
}
</script>
<template>
<svg @click="onClick">
<foreignObject x="0" y="40%" width="100%" height="200">
<p class="tip">
Click on the canvas to draw a circle. Click on a circle to select it.
Right-click on the canvas to adjust the radius of the selected circle.
</p>
</foreignObject>
<circle
v-for="circle in circles"
:cx="circle.cx"
:cy="circle.cy"
:r="circle.r"
:fill="circle === selected ? '#ccc' : '#fff'"
@click="selected = circle"
@contextmenu.prevent="adjust(circle)"
:key="circle"
></circle>
</svg>
<div class="controls">
<button @click="undo" :disabled="index <= 0">Undo</button>
<button @click="redo" :disabled="index >= history.length - 1">Redo</button>
</div>
<div class="dialog" v-if="adjusting" @click.stop>
<p>Adjust radius of circle at ({{ selected.cx }}, {{ selected.cy }})</p>
<input type="range" v-model="selected.r" min="1" max="300" />
</div>
</template>
<style>
body {
margin: 0;
overflow: hidden;
}
svg {
width: 100vw;
height: 100vh;
background-color: #eee;
}
circle {
stroke: #000;
}
.controls {
position: fixed;
top: 10px;
left: 0;
right: 0;
text-align: center;
}
.controls button + button {
margin-left: 6px;
}
.dialog {
position: fixed;
top: calc(50% - 50px);
left: calc(50% - 175px);
background: #fff;
width: 350px;
height: 100px;
padding: 5px 20px;
box-sizing: border-box;
border-radius: 4px;
text-align: center;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25);
}
.dialog input {
display: block;
width: 200px;
margin: 0px auto;
}
.tip {
text-align: center;
padding: 0 50px;
color: #bbb;
}
</style>
- 'shallowReactive' : 'reactive'와 달리 객체를 얕게 반응성으로 관리합니다. 이 경우, 이 예제에서는 'history' 배열 자체는 반응성을 가지지만, 배열 내부의 객체(예: 각 상태 배열의 요소들)는 반응성을 갖지 않습니다. 이는 메모리 사용량을 줄이고, 불필요한 성능 비용을 방지하는데 도움이 됩니다. 만약 'reactive'로 관리되었다면 배열 내부의 모든 객체와 그 속성들도 반응성을 가지게 되어 객체 내부의 속성 변화도 자동으로 추적하고 반응할 수 있게 됩니다.
이렇게 주요 기능들이 포함되어 있는 예제 위주로 공식문서를 살펴보았습니다👐
짧은 기간 동안 공부를 해야 될 때는 예제 위주로 살펴보고 빠르게 익히고 넘어가는 편인데요, 공식문서 예제가 꽤 알차서 많은 도움이 되었던 것 같습니다 !
혹시라도 이 글을 보시게 되는 분들에게도 도움이 되었으면 좋겠네요..🤭