학습일지/프론트엔드

[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'로 관리되었다면 배열 내부의 모든 객체와 그 속성들도 반응성을 가지게 되어 객체 내부의 속성 변화도 자동으로 추적하고 반응할 수 있게 됩니다.

 


이렇게 주요 기능들이 포함되어 있는 예제 위주로 공식문서를 살펴보았습니다👐

짧은 기간 동안 공부를 해야 될 때는 예제 위주로 살펴보고 빠르게 익히고 넘어가는 편인데요, 공식문서 예제가 꽤 알차서 많은 도움이 되었던 것 같습니다 !

혹시라도 이 글을 보시게 되는 분들에게도 도움이 되었으면 좋겠네요..🤭