컴포넌트: UserProfile.vue
API 사용하기
dicebear의 API를 사용합니다.
pnpm add
@dicebear/core
@dicebear/collection
axios
[Optional] api layer 작성하기
dicebear에서 제공하는 sdk를 사용할 것이기 때문에 axios를 직접 사용하지는 않습니다.
import axios from "axios";
export const httpInstance = axios.create({
baseURL: "/api",
})
export const http = {
get: (url,params) => httpInstance.get(url, params),
post: (url, params) => httpInstance.post(url, params)
}
UserStore 작성하기
stores/user.js
import { defineStore } from "pinia";
import { createAvatar } from '@dicebear/core';
import {
adventurer,
adventurerNeutral,
avataaars,
avataaarsNeutral,
bigEars,
bigEarsNeutral,
bigSmile,
bottts,
botttsNeutral,
croodles,
croodlesNeutral
} from '@dicebear/collection';
const profiles = [
adventurer,
adventurerNeutral,
avataaars,
avataaarsNeutral,
bigEars,
bigEarsNeutral,
bigSmile,
bottts,
botttsNeutral,
croodles,
croodlesNeutral
];
import 구문 부터 살펴보면 dicebear에서 제공하는 여러 스타일을 가져오기 위해 여러 모듈들을 임포트합니다.
이어서 작성합니다.
const getRandomUserData = async (userId) => {
const avatar = await createAvatar(profiles[Math.floor(profiles.length * Math.random())])
return {
name: `Dante${userId}`,
age: Math.floor(Math.random() * 100),
userId,
avatar: await avatar.toDataUri()
}
};
userId
가 입력되면 임의의 유저 데이터를 받아오기 위한 헬퍼 함수를 작성합니다.
이어서 작성합니다.
export const useUserStore = defineStore("user", {
state:() => ({
userData: null,
loading: false,
error: null
}),
getters: {
isUserLoggedIn: state => !!state.userData,
},
actions: {
async fetchUserData(userId) {
this.loading = true;
const response = await new Promise(resolve => {
setTimeout(() => {
resolve(getRandomUserData(userId))
},700)
});
this.userData = response;
this.loading = false;
},
}
})
액션에 비동기함수 fetchUserData를 선언합니다. 로딩 시간을 길게 주어 loading state가 올바르게 표기되는지 확인하기 위해 Promise 내부에 임의로 700ms 의 대기시간을 넣었습니다.
UserProfile.vue 작성하기
<template>
<div class="flex items-center p-2 bg-white rounded-lg shadow-sm hover:bg-gray-100">
<img
v-if="userData && userData.avatar"
:src="userData.avatar"
alt="User Avatar"
class="w-8 h-8 rounded-full"
/>
<span v-if="userData" class="ml-3 text-sm font-medium text-gray-700">
{{ userData.name }}
</span>
<span v-if="userData.loading" class="ml-3 text-sm font-medium text-gray-700">
Loading...
</span>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { useUserStore } from '../../stores/user'
const userStore = useUserStore();
const { userData } = storeToRefs(userStore);
</script>
테일윈드를 이용해 스타일링을 합니다.
스토어 선언할 때 작성한 useUserStore를 호출해 userStore를 가져옵니다.
storeToRefs를 이용해 userData를 구조분해하여 userStore.userData
와 같이 작성하지 않아도 reactivity를 유지하게 합니다.
template 내부에 v-if
디렉티브 사용한 부분만 간단히 보면 userData가 없는 동안에는 loading 문구를 표시합니다.
<span v-if="userData" class="ml-3 text-sm font-medium text-gray-700">
{{ userData.name }}
</span>
<span v-if="userData.loading" class="ml-3 text-sm font-medium text-gray-700">
Loading...
</span>
이 컴포넌트는 userData를 내부에서 fetch하지 않고 데이터를 보여주는 presentation만 담당합니다.
이제 작성한 컴포넌트를 테스트하기 위해 스토리북 코드를 작성하겠습니다. UserProfile.vue와 동일한 경로에 UserProfile.stories.js를 생성합니다.
import UserProfile from './UserProfile.vue';
import { useUserStore } from '@/stores/user';
export const Base = {
render: (args) => ({
components: { UserProfile },
setup() {
const userStore = useUserStore();
userStore.userData = {
name: "John Doe",
email: "john@example.com",
id: 53,
avatar: "https://api.dicebear.com/7.x/fun-emoji/svg"
}
return { args }
},
template: /* html */`
<UserProfile
v-bind="args"
/>
`,
}),
args: {
alt: "storybook Profile"
}
}
export default {
component: UserProfile
}
일부분씩 뜯어서 보겠습니다.
최하단의 export default
export default {
component: UserProfile
}
UserProfile 스토리가 생긴것을 볼 수 있습니다.
개별 스토리
export const Base = {...
UserProfile 내부에 Base story를 생성합니다.
테스트 store 주입
setup() {
const userStore = useUserStore();
userStore.userData = {
name: "John Doe",
email: "john@example.com",
id: 53,
avatar: "https://api.dicebear.com/7.x/fun-emoji/svg"
}
return { args }
},
template: /* html */`
<UserProfile
v-bind="args"
/>
`,
userStore.userData = {
를 사용해 테스트에 사용할 스토어를 초기화 합니다. 모킹 데이터를 넣어준 것입니다.
결과