단어 저장
2023년 3월 10일
intro
검색한 단어를 저장하는 기능 구현
구현할 기능 미리보기
이전에는 flask api 서버를 따로 두어 MySQL을 이용해 단어를 저장했었다. 이번에는 api 서버 없이 vue만 사용하고 싶었기에 저장 방식을 바꾸기로 했다. Firebase나 localStorage 중 하나를 이용하려고 한다.
Firestore
Firebase의 Firestore는 NoSQL 클라우드 데이터베이스이다. NoSQL이기에 사용하는 방법도 간단하고 공식 문서도 상당히 잘 되어있어 테스트해 봤을 때 상당히 좋았다. 무료 요금제인 Spark 요금제는 총 1GB Byte까지 저장할 수 있으며 하루에 쓰기 20,000, 읽기 50,000, 삭제 20,000 이 가능하다. 토이 프로젝트에서 이용하기에 적합해 보였다.
localStorage
localStorage는 브라우저 상에 저장하는 방식 중 하나다. localStorage는 간단한 방식으로 영구적으로 정보를 브라우저에 보존할 수 있다. 가급적 1MB 이상의 큰 데이터를 쓰는 것은 피하는 게 좋다고 하지만 토이 프로젝트에서 이용할 땐 크게 연관이 없다고 생각한다.
두 방식 다 사용하기에 간단하지만 localStorage가 비교적 좀 더 쉽고 Firestore는 이 프로젝트에 필요한 기능에 과하다고 생각이 들어 두 방식 중에 고민하다 localStorage를 이용하는 것으로 결정했다.
code
stores/Main.js
stores/Main.js
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { useStorage } from '@vueuse/core';
import { useModalStore } from './Modal';
export const useMainStore = defineStore('main', () => {
const modalStore = useModalStore();
const localWords = useStorage('mapWords', new Map(), localStorage);
const wordArr = ref([]);
function getTimeAndTimestamp() {
const date = new Date();
const nowTime = `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
const timestamp = date.getTime();
return { date, nowTime, timestamp };
}
function wordArrUpdate() {
const localWordsToArr = Array.from(localWords.value, (item) => {
return { ...item[1] };
} );
wordArr.value = [...localWordsToArr].reverse();
}
// init: localStorage에 있는 단어들 set
function setWordDic() {
wordArrUpdate();
}
// word add
function wordAdd(word, means) {
const { nowTime, timestamp } = getTimeAndTimestamp();
const item = { word, means: means.toString(), timestamp, time: nowTime, check: false };
// 이미 단어가 존재하면 지웠다가 저장
if (localWords.value.has(word)) {
localWords.value.delete(word);
}
localWords.value.set(word, item);
wordArrUpdate();
modalStore.modalExit();
}
return { setWordDic, wordAdd, wordArr}
});
- 로컬 스토리지를 이용하는 메인 스토어
localWords
: vueuse의useStorage
를 이용해 로컬 스토리지를 반응형으로 이용, 자료 형태는 Map 객체, 다음에 구현할 삭제를 좀 더 간단하게 하기 위해 Map 객체를 이용wordArr
:localWords
의 단어 순서를 정렬하기 위해 배열 형태로도 따로 저장, 삭제나 저장은 Map 객체로도 가능하지만 가장 최근 단어가 맨 위로 가게 하기 위해서는 배열 형태가 있는 것이 좀 더 편함. 화면에 렌더링 될 단어들의 데이터function setWordDic()
: App.vue에서 호출될 함수로 앱이 처음 시작할 때 로컬 스토리지의 데이터들을wordArr
에 저장function wordArrUpdate()
: 로컬 스토리지의 데이터localWords
의 값들을 순서를reverse()
한 배열의 형태로 반환funcion wordAdd(word, means)
: 로컬 스토리지에 { word, means, timestamp, time, check } 형태로 저장, 다음에 구현할 체크 기능과 언제 저장했는지 알기 위한 time, timestamp도 저장. 이미 저장되어 있는 단어를 다시 저장할 경우 단어를 맨 위로 올리기 위해 단어를 삭제했다 다시 저장, 단어를 저장한 뒤wordArrUpdate()
를 통해wordArr
도 갱신.
components/WordList.vue
components/WordList.vue
<script setup>
import { useMainStore } from '../stores/Main';
import WordCard from './WordCard.vue';
const mainStore = useMainStore();
</script>
<template>
<div class="md:ml-10 lg:ml-32 xl:ml-36 2xl:ml-36">
<div class="grid md:grid-cols-2 md:gap-x-36">
<div v-for="(word, index) in mainStore.wordArr" :key="word" class="mb-[32px]">
<WordCard :word="word.word" :means="word.means" :check="word.check" :index="index"></WordCard>
</div>
</div>
</div>
</template>
- main 스토어의
wordArr
을 여러 개의WordCard.vue
로 뿌려주는 몸통 역할의 컴포넌트
components/WordCard.vue
components/WordCard.vue
<script setup>
import { Icon } from '@iconify/vue';
import { ref } from 'vue';
const props = defineProps({
word: String,
means: String,
check: Boolean,
index: Number,
})
const word = ref(props.word);
const means = ref(props.means);
const check = ref(props.check);
</script>
<template>
<div class="flex flex-col">
<div class="flex gap-x-1">
<!-- check Icon -->
<button>
<Icon v-if="check" class="flex items-center" icon="carbon:checkbox" width="37" height="37"></Icon>
</button>
<!-- word -->
<span class="card_word">{{ word }}</span>
</div>
<!-- mean -->
<div>
<li v-for="mean in means.split(',')" :key="mean" class="card_content">
{{ mean }}
</li>
</div>
</div>
</template>
ModalCaseNomal.vue
components/ModalCaseNomal.vue
<script setup>
import TheModal from './TheModal.vue';
import { useMainStore } from '../stores/Main';
import { useInputWordStore } from '../stores/InputWord';
import { useModalStore } from '../stores/Modal';
import { ref } from 'vue';
const mainStore = useMainStore();
const modalStore = useModalStore();
const wordStore = useInputWordStore();
const word = ref('');
const means = ref('');
const [modalWord, modalMeans] = wordStore.caseNomalCreate();
word.value = modalWord;
means.value = modalMeans;
</script>
<template>
<TheModal>
<template #word>
{{ word }}
</template>
<template #means>
<li v-for="mean in means" :key="mean" class="modal_means">
{{ mean }}
</li>
</template>
<template #footer>
<div class="flex gap-x-4 justify-end">
<button @click="modalStore.modalExit" class="modal_btn bg-neutral-400 hover:bg-neutral-500">cancel</button>
<button @click="mainStore.wordAdd(word, means)" class="modal_btn bg-emerald-400 hover:bg-emerald-600">add</button>
</div>
</template>
</TheModal>
</template>
ModalCaseNomal.vue
의 add 버튼에@click="mainStore.wordAdd(word, means)"
추가
App.vue
App.vue
<script setup>
import TheHeader from './components/TheHeader.vue';
import WordList from './components/WordList.vue';
import { useMainStore } from './stores/Main';
const mainStore = useMainStore();
mainStore.setWordDic();
</script>
<template>
<header class="pb-16">
<TheHeader></TheHeader>
</header>
<main class="pt-[32px] px-10 md:px-24 lg:px-32 2xl:px-80 min-h-screen">
<WordList></WordList>
</main>
</template>
<style scoped>
* {
--bg-color: #F2E399;
--bg-transparent: rgba(255, 255, 255, 0);
--border-color: #BFBFBF;
}
main {
font-size: 16px;
line-height: 32px;
background: linear-gradient(
to bottom,
var(--border-color) 0%,
var(--border-color) 1px,
var(--bg-transparent) 1px,
var(--bg-transparent) 100%
);
background-size: 100% 32px;
background-color: var(--bg-color);
overflow-y: auto;
}
@media (max-width: 281px) {
* {
font-size: 14px;
}
}
</style>
- main 부분에
WordList
추가 - App이 시작될 때 main 스토어의
setWordDic()
을 호출해 로컬 스토리지에서 단어들을 가지고 옴