특정 화면에서 반복적인 작업을 수행해야하는 아래와 같은 경우를 가정해보자.
- Index.vue 는 15초를 주기로 새로운 배경 이미지를 보여 준다.
개발은 크게 고민하지 않고 공개된 Unsplash API 을 활용 이미지 목록을 가져와서 주기적으로 화면의 배경 이미지를 바꿔주었다.
- src/store/unsplash.js
- src/view/Indes.vue
Index View 에서는 ① mounted 되면 upsplash 에서 가져온 이미지 목록이 있는지 확인하고 없는 경우 fetch 함수를 호출하여 이미지 목록을 가져온다. ② setTimeout 함수를 사용하여 15000 단위로 바탕하면 이미지를 바꿔준다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Utilities | |
import { defineStore } from 'pinia' | |
// Install unsplash | |
import { createApi } from 'unsplash-js' | |
//import whatwgFetch from "whatwg-fetch"; | |
const unsplash = createApi({ | |
accessKey: import.meta.env.VITE_UNSPLASH_API_KEY, | |
//fetch: whatwgFetch, | |
}) | |
export const useUnsplashStore = defineStore({ | |
// id is required so that Pinia can connect the store to the devtools | |
id: 'unsplash', | |
state: () => ({ | |
photos: [], | |
isLoaded: false, | |
}), | |
getters: { | |
total: state => state.photos.length, | |
}, | |
actions: { | |
getRandomPhoto () { | |
return this.photos[Math.floor(Math.random() * this.photos.length)] | |
}, | |
async fetch (queryString) { | |
if (this.isLoaded) return | |
queryString = queryString || 'dark,girls,sexy' | |
await unsplash.photos | |
.getRandom({ query: queryString, count: 50 }) | |
.then(result => { | |
if (result.type === 'success') { | |
this.photos = result.response | |
this.isLoaded = true | |
} | |
}) | |
}, | |
}, | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<v-app> | |
<NavbarDefault></NavbarDefault> | |
<v-main> | |
<v-parallax height="100vh" :style="headerStyle"> | |
</v-parallax> | |
<div class="mr-auto ml-10 mb-50" style="position:absolute; bottom:150px; font-size: .8rem;"> | |
<v-card color="transparent" theme="dark" max-width="450" v-if="bgPhoto.visible" flat> | |
<v-card-text class="text-h7 py-2">{{ bgPhoto.unsplash.description || bgPhoto.unsplash.alt_description }}</v-card-text> | |
<v-card-actions> | |
<v-list-item class="w-100"> | |
<template v-slot:prepend> | |
<v-avatar color="grey-darken-3"> | |
<v-img :src="bgPhoto.unsplash.user.profile_image.small" alt="{{ bgPhoto.unsplash.user.username }}"></v-img> | |
</v-avatar> | |
</template> | |
<v-list-item-title style="font-size: .8rem;">{{ bgPhoto.unsplash.user.name }}</v-list-item-title> | |
<v-list-item-subtitle><small>{{ bgPhoto.unsplash.location.name }}</small></v-list-item-subtitle> | |
<template v-slot:append> | |
<div class="justify-self-end ml-5"> | |
<v-icon class="me-1" icon="mdi-thumb-up"></v-icon> | |
<span class="subheading me-2">{{ bgPhoto.unsplash.likes }}</span> | |
<span class="me-1">·</span> | |
<v-icon class="me-1" icon="mdi-account-eye"></v-icon> | |
<span class="subheading">{{ bgPhoto.unsplash.views }}</span> | |
</div> | |
</template> | |
</v-list-item> | |
</v-card-actions> | |
</v-card> | |
</div> | |
</v-main> | |
<FooterDefault></FooterDefault> | |
</v-app> | |
</template> | |
<script setup lagn="ts"> | |
// Composables | |
import NavbarDefault from "../layouts/navbars/NavbarDefault.vue"; | |
import FooterDefault from "../layouts/footers/FooterDefault.vue"; | |
// Utilities | |
import { useUnsplashStore } from "@/store/unsplash"; | |
import { | |
onMounted, | |
computed, | |
ref, | |
reactive | |
} from "vue"; | |
// Globals | |
const unsplash = useUnsplashStore(); | |
const bgPhoto = reactive({ | |
url: "https://cdn.vuetifyjs.com/images/backgrounds/vbanner.jpg", | |
visible: false, | |
unsplash: null, | |
}); | |
const headerStyle = computed(() => { | |
return { | |
backgroundImage: `url(${bgPhoto.url})`, | |
transition: "background 1000ms ease-in 500ms", | |
backgroundSize: "cover", | |
}; | |
}) | |
onMounted(async () => { | |
if (!unsplash.isLoaded) { | |
await unsplash.fetch(); | |
} | |
bgUpdateFromUnsplash(); | |
}); | |
function bgUpdateFromUnsplash() { | |
if (unsplash.total > 0) { | |
setTimeout(function () { | |
var proxyImage = new Image(); | |
let unsplashPhoto = unsplash.getRandomPhoto(); | |
proxyImage.src = unsplashPhoto.urls.regular; | |
proxyImage.onload = function () { | |
bgPhoto.url = proxyImage.src; | |
bgPhoto.unsplash = unsplashPhoto; | |
bgPhoto.visible = true; | |
bgUpdateFromUnsplash(); | |
}; | |
}, 15000); | |
} | |
} | |
</script> |
위 코드의 문제점은 아래와 같다.
- 다른 View 로 이동하더라도 백그라우드에서 ② 작업이 무한 실행된다.
- 다른 View 에서 Index View 로 이동하면 ① 작업을 수행하고 ② 작업을 다시 무한 실행한다.
- 불필요한 메모리 누수가 발생한다.
◼︎ 개발환경
- Model : MacBook Pro (14-inch, 2021)
- CPU : Apple M1 Pro
- MENORY : 16GB
- DISK : 512 GB SSD
- OS : macOS 13.2 (22D49)
문제 해결
Vue3 Lifecycle 에 따르면 Composition API 에서 setup 은 create 와 같으며 View 가 마운트 되면 (View 가 화면에 보여지면) mounted (onMounted) 가 실행되고 언마우트 되면 (다른 View 로 이동하면) unmounted (onUnmounted)가 실행되는데 이점을 이용하면 Index 가 화면에 보여질 때만 ② 작업을 수행하도록 할 수 있다.
반복작업은 setInterval() 함수를 사용하고 더이상 반복작업이 필요없는 경우에는 (다른 View 로 이동할 때) , setInterval() 호출시 리턴되는 ID 값을 인자로 clearInterval() 함수를 호출하면 timer 을 초기활 할 수 있다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<v-parallax :style="headerStyle" class="h-screen w-auto" /> | |
<div class="mr-auto ml-10 mb-50" style="position:absolute; bottom:150px; font-size: .8rem;"> | |
<v-card v-if="bgPhoto.visible" color="transparent" theme="dark" max-width="450" flat> | |
<v-card-text class="text-h7 py-2">{{ | |
bgPhoto.unsplash.description || | |
bgPhoto.unsplash.alt_description | |
}}</v-card-text> | |
<v-card-actions> | |
<v-list-item class="w-100"> | |
<template #prepend> | |
<v-avatar color="grey-darken-3"> | |
<v-img :src="bgPhoto.unsplash.user.profile_image.small" alt="{{ bgPhoto.unsplash.user.username }}" /> | |
</v-avatar> | |
</template> | |
<v-list-item-title style="font-size: .8rem;">{{ | |
bgPhoto.unsplash.user.name | |
}}</v-list-item-title> | |
<v-list-item-subtitle><small>{{ | |
bgPhoto.unsplash.location.name | |
}}</small></v-list-item-subtitle> | |
<template #append> | |
<div class="justify-self-end ml-5"> | |
<v-icon class="me-1" icon="mdi-thumb-up" /> | |
<span class="subheading me-2">{{ bgPhoto.unsplash.likes }}</span> | |
<span class="me-1">·</span> | |
<v-icon class="me-1" icon="mdi-account-eye" /> | |
<span class="subheading">{{ bgPhoto.unsplash.views }}</span> | |
</div> | |
</template> | |
</v-list-item> | |
</v-card-actions> | |
</v-card> | |
</div> | |
</template> | |
<script setup lang="ts"> | |
// Utilities | |
import { useUnsplashStore } from '@/store/unsplash' | |
import { | |
computed, | |
onMounted, | |
onUnmounted, | |
reactive, | |
} from 'vue' | |
// Globals | |
const unsplash = useUnsplashStore() | |
const bgPhoto = reactive({ | |
url: 'https://cdn.vuetifyjs.com/images/backgrounds/vbanner.jpg', | |
visible: false, | |
unsplash: null, | |
}) | |
const headerStyle = computed(() => { | |
return { | |
backgroundImage: `url(${bgPhoto.url})`, | |
transition: 'background 1000ms ease-in 500ms', | |
backgroundSize: 'cover', | |
} | |
}) | |
const intervalTime: number = 15000 | |
let intervalId: number | |
onMounted(async () => { | |
if (!unsplash.isLoaded) { | |
await unsplash.fetch() | |
} | |
updateBgImage() | |
intervalId = setInterval(updateBgImage, intervalTime) | |
}) | |
onUnmounted(() => { | |
clearInterval(intervalId) | |
}) | |
function updateBgImage() { | |
if (unsplash.total > 0) { | |
const proxyImage = new Image() | |
const unsplashPhoto = unsplash.getRandomPhoto() | |
proxyImage.src = unsplashPhoto.urls.regular | |
proxyImage.onload = function () { | |
bgPhoto.url = proxyImage.src | |
bgPhoto.unsplash = unsplashPhoto | |
bgPhoto.visible = true | |
} | |
} | |
} | |
</script> |
댓글 없음:
댓글 쓰기