Pinia master course
  • 🍏Introduction
    • Why Pinia
      • Introduction & Project setup
      • Vuex VS Pinia
  • 🍎Essential of Pinia
    • Create Store, State, Action, Getters
    • Use store in vue component
      • [Optional] 구조분해는 왜 reactivity를 잃게 만들까?
      • 비동기 액션 생성하기
      • 로컬스토리지 사용하기
Powered by GitBook
On this page
  • 목표 컴포넌트 UserForm.vue
  • user store 수정하기
  • UserForm.vue 작성
  • template
  • 스토리 만들기 UserForm.stories.js
  • 결과
  • 개선 포인트
  1. Essential of Pinia
  2. Use store in vue component

로컬스토리지 사용하기

pinia에서 persistance 사용하기

Previous비동기 액션 생성하기

Last updated 1 year ago

비동기 액션 생성하기에서 이어집니다. 안보신 분들은 먼저 살펴보세요.

목표 컴포넌트 UserForm.vue

원하는 userId를 입력하면 해당 id로 로그인합니다.

Clear History를 누르면 로컬스토리지에 저장된 값을 삭제합니다.

user store 수정하기

localStorage를 사용하기 위해 userStore를 수정합니다.


const token = '$__userData';

export const useUserStore = defineStore("user", {
  state:() => ({
    userData: JSON.parse(localStorage.getItem(token) || null),
    userHistory: [],
    loading: false,
    error: null
  }),
  getters: {
    isUserLoggedIn: state => !!state.userData,
    hasHistory: state => state.userHistory.length > 0,
  },
  actions: {
    async fetchUserData(userId) {
      this.loading = true;
      const saved = localStorage.getItem(token);
      if(saved) {
        this.userData = JSON.parse(saved);
        this.loading = false;
        return;
      }


      const response = await new Promise(resolve => {
        setTimeout(() => {
          resolve(getRandomUserData(userId))
        },700)
      });
      this.userData = response;
      localStorage.setItem(token, JSON.stringify(this.userData));
      this.loading = false;
    },
    async updateUserData(userId) {
      this.loading = true;
      const response = await new Promise(resolve => {
        setTimeout(() => {
          resolve(getRandomUserData(userId))
        }, 700);
      })
      this.userData = response;
      this.userHistory.push(this.userData);
      this.loading = false;
    },
    clearHistory() {
      this.userHistory = [];
    },
    logout() {
      this.userData = null;
      localStorage.removeItem(token);
    }
  }
})

코드를 뜯어서 살펴보겠습니다.

const token = '$__userData';

로컬스토리지에 저장할 때 사용할 토큰의 키값입니다.

state:() => ({
    userData: JSON.parse(localStorage.getItem(token) || null),
    userHistory: [],
    loading: false,
    error: null
  }),

변경된 부분을 보겠습니다.

유저데이터를 초기화할때 로컬스토리지에 있는 값을 읽습니다.

getters: {
    isUserLoggedIn: state => !!state.userData,
    hasHistory: state => state.userHistory.length > 0,
},

hasHistory 정보를 넣어 히스토리 정보가 있는지 확인하고 isUserLoggedIn. 을 통해 현재 로그인되어있는지 확인합니다. 이제 actions를 살펴보겠습니다.

Actions

async fetchUserData(userId) {
      this.loading = true;
      const saved = localStorage.getItem(token);
      if(saved) {
        this.userData = JSON.parse(saved);
        this.loading = false;
        return;
      }


      const response = await new Promise(resolve => {
        setTimeout(() => {
          resolve(getRandomUserData(userId))
        },700)
      });
      this.userData = response;
      localStorage.setItem(token, JSON.stringify(this.userData));
      this.loading = false;
    },

로컬스토리지에 값이 있으면 데이터를 읽어온 후 문제가 없으면 데이터를 패칭하지 않습니다.

그렇지 않다면 데이터를 받아온 후 로컬스토리지에 토큰 값을 이용해 받아온 데이터로 업데이트합니다.

clearHistory() {
      this.userHistory = [];
    },
logout() {
  this.userData = null;
  localStorage.removeItem(token);
}

clearHistory는 사이드이팩트가 없습니다. Vuex에서 mutations과 같은 행동을 합니다.

그와 반대로 logout은 사이드이펙트를 사용합니다.

Pinia에서는 mutations, actions로 나뉘어지지 않음을 확인할 수 있습니다.

UserForm.vue 작성

업데이트한 스토어를 사용하는 UserForm.vue를 작성하겠습니다.

<template>
  <div class="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-4">
    <div v-if="loading" class="text-lg text-blue-600">Loading...</div>
    <div v-else class="max-w-md w-full bg-white rounded-lg shadow-md p-6">
      <div v-if="error" class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg" role="alert">
        Error: {{ error }}
      </div>
      <div v-else-if="userData">
        <h2 class="text-xl font-semibold mb-4">Edit User Id</h2>
        <form @submit.prevent="updateUser" class="space-y-4">
          <input 
            type="number"
            min="1"
            v-model="userIdInput"
            class="w-full p-2 border border-gray-300 rounded-md" 
          />
          <button 
            type="submit" 
            class="w-full text-white rounded-md py-2"
            :class="[submitDisabled ? 'bg-red-600 hover:bg-red-600' :'bg-blue-600 hover:bg-blue-700']"
            :disabled="submitDisabled"
            :aria-disabled="submitDisabled"
          >
            Save Changes
          </button>
        </form>
        <button v-if="hasHistory" @click="clearUserHistory" class="mt-4 w-full bg-red-600 text-white rounded-md py-2 hover:bg-red-700">Clear History</button>
        <UserProfile v-if="hasHistory"/>
      </div>
    </div>
  </div>
</template>


<script setup>
import { ref, onMounted, computed } from 'vue';
import { useUserStore } from '../../stores/user';
import { storeToRefs } from 'pinia';
import UserProfile from "./UserProfile.vue"

const userStore = useUserStore();
const { userData, loading, error, hasHistory } = storeToRefs(userStore);
const { fetchUserData, updateUserData, clearHistory } = userStore;
const userIdInput = ref(userData.value)
const submitDisabled = computed(() => userIdInput.value < 1)
onMounted(() => {
  if (!userData.value) {
    fetchUserData(123);
  }
});

const updateUser = () => {
  updateUserData(userIdInput.value);
};

const clearUserHistory = () => {
  clearHistory();
};
</script>

비동기 액션 생성하기에서 생성한 UserProfile.vue를 재활용합니다.

storeToRefs

const userStore = useUserStore();
const { userData, loading, error, hasHistory } = storeToRefs(userStore);
const { fetchUserData, updateUserData, clearHistory } = userStore;

구조분해 할당을 사용하기 위해 storeToRefs를 사용해 state 값들을 받아옵니다. 액션은 감싸주지 않고 구조분해해도 괜찮습니다.

로컬 상태 값

const formUserData = ref({ ...userData.value });
const userIdInput = ref(userData.value)
const submitDisabled = computed(() => userIdInput.value < 1)

userIdInput은 인풋 태그에 사용하기 위한 로컬 상태 값입니다. 유효성 검사를 위해 computed 값인 submitDisabled 를 만들어 값이 비어있을 때는 버튼을 클릭하지 못하게 만들겠습니다.

데이터 패칭

onMounted(() => {
  if (!userData.value) {
    fetchUserData(123);
  }
});

mount된 이후 userData를 확인해 데이터가 없으면 fetchUserData를 호출합니다.

이제 template을 살펴보겠습니다.

template

데이터가 없을 때

<div v-if="loading" class="text-lg text-blue-600">Loading...</div>

loading 데이터를 확인해 로딩 상태를 표시해줍니다.

데이터가 있을 때

<div v-else class="max-w-md w-full bg-white rounded-lg shadow-md p-6">
  <div v-if="error" class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg" role="alert">
    Error: {{ error }}
  </div>
  <div v-else-if="userData">
    <h2 class="text-xl font-semibold mb-4">Edit User Id</h2>
    <form @submit.prevent="updateUser" class="space-y-4">
      <input 
        type="number"
        min="1"
        v-model="userIdInput"
        class="w-full p-2 border border-gray-300 rounded-md" 
      />
      <button 
        type="submit" 
        class="w-full text-white rounded-md py-2"
        :class="[submitDisabled ? 'bg-red-600 hover:bg-red-600' :'bg-blue-600 hover:bg-blue-700']"
        :disabled="submitDisabled"
        :aria-disabled="submitDisabled"
      >
        Save Changes
      </button>
    </form>
    <button v-if="hasHistory" @click="clearUserHistory" class="mt-4 w-full bg-red-600 text-white rounded-md py-2 hover:bg-red-700">Clear History</button>
    <UserProfile v-if="hasHistory"/>
  </div>
</div>

form 태그 내부를 먼저 보겠습니다.

<input 
  type="number"
  min="1"
  v-model="userIdInput"
  class="w-full p-2 border border-gray-300 rounded-md" 
/>

v-model 디렉티브를 사용해 userIdInput를 양방향 바인딩 합니다.

<button 
    type="submit" 
    class="w-full text-white rounded-md py-2"
    :class="[submitDisabled ? 'bg-red-600 hover:bg-red-600' :'bg-blue-600 hover:bg-blue-700']"
    :disabled="submitDisabled"
    :aria-disabled="submitDisabled"
>
    Save Changes
</button>

computed 값으로 만들어놨던 submitDisabled를 사용해 input 태그가 비어있거나 1보다 작은 값일 때 submit을 못하게 만듭니다.

이제 form 태그 밖을 보겠습니다.

<button v-if="hasHistory" @click="clearUserHistory" class="mt-4 w-full bg-red-600 text-white rounded-md py-2 hover:bg-red-700">Clear History</button>
<UserProfile v-if="hasHistory"/>

히스토리가 있는 경우 UserProfile 컴포넌트를 보여줍니다.

이제 UI 테스트를 위해 스토리를 만들겠습니다.

스토리 만들기 UserForm.stories.js

import UserForm from './UserForm.vue';

export const Base = {
  render: (args) => ({
    components: { UserForm },
    setup() {
      return { args }
    },
    template: /* html */`
      <UserForm
        v-bind="args"
      />
    `,
  }),
  args: {
    alt: "storybook User Form"
  }
}

export default {
  component: UserForm
}

추가 설명이 필요없을 정도로 간단합니다. UserProfile.stories.js와 비교했을 때도 데이터를 주입해주는 부분이 없기 때문에 굉장히 간단해졌습니다.

결과

개선 포인트

현재 데이터 입력 이후 123 userInput 글자가 계속 남아있습니다. 이 부분이 자동으로 제거되도록 만들어보세요.

🍎