에서 이어집니다. 안보신 분들은 먼저 살펴보세요.
원하는 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를 작성하겠습니다.
<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>
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 테스트를 위해 스토리를 만들겠습니다.
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 글자가 계속 남아있습니다. 이 부분이 자동으로 제거되도록 만들어보세요.