단어 저장

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()을 호출해 로컬 스토리지에서 단어들을 가지고 옴


댓글