<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
<div id="app">
<div v-cloak
class="photos">
<div v-for="photo in photos"
:key="photo.id"
class="photo">
<div class="photo__title">{{ photo.title }}</div>
<div :style="`background-image: url(${photo.url});`"
:class="{ loaded: photo.imageLoaded }"
class="photo__image"></div>
</div>
</div>
<div id="scroll-observer"
v-if="showScrollObserver"
ref="scrollObserver">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
</div>
</div>
.photos {
padding: 30px;
.photo {
margin-bottom: 40px;
&__title {
font-size: 18px;
font-weight: 700;
margin-bottom: 10px;
color: #444;
line-height: 1.4;
}
&__image {
width: 100%;
height: 250px;
border-radius: 10px;
overflow: hidden;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
&.loaded {
animation: jello 1s;
}
}
}
}
#scroll-observer {
height: 200px;
display: flex;
justify-content: center;
align-items: center;
}
// For Vue
[v-cloak] {
display: none;
}
// Loader
.lds-ring {
display: inline-block;
position: relative;
width: 64px;
height: 64px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 51px;
height: 51px;
margin: 6px;
border: 6px solid #fff;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #F86C06 transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
View Compiled
new Vue({
el: '#app',
data () {
return {
photos: [],
length: 4,
showScrollObserver: true
}
},
created () {
this.fetchPhotos(1, this.length)
.then(() => {
this.initIntersectionObserver()
})
// For next photo
this.increasePhotoLength()
},
methods: {
/**
* @summary 사진을 가져옵니다.
* @param {number} start - 가져올 사진의 시작 포인트
* @param {number} limit - 가져올 사진의 개수
* @return {Promise} - 비동기 실행을 위한 Promise 객체
*/
fetchPhotos (start = 1, limit = 1) {
return new Promise(resolve => {
const photosToFetch = []
for (let i = start; i < start + limit; i += 1) {
photosToFetch.push(
axios(`https://jsonplaceholder.typicode.com/photos/${i}`)
)
}
Promise.all(photosToFetch)
.then(photos => {
return photos.map(photo => {
return {
...photo.data,
imageLoaded: false
}
})
})
.then(photos => {
this.photos.push(...photos)
this.photos.forEach(photo => {
// 사진 로드를 대기
const image = new Image()
image.src = photo.url
image.onload = () => {
photo.imageLoaded = true
}
})
resolve(true)
})
})
},
/**
* @summary Intersection Observer를 초기화합니다.
*/
initIntersectionObserver () {
const io = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
// 요소(#scroll-observer)의 가시성이 확보되면..
if (entry.isIntersecting) {
this.fetchPhotos(this.length)
// For next photo
this.increasePhotoLength()
}
})
}, {
// 'Codepen'와 'JSFiddle'에서는 옵션이 제대로 동작하지 않는군요.
// 옵션에 대한 테스트는 'JSBin'이나 로컬에서 진행하시면 됩니다.
// 로딩 애니메이션이 보이지 않게 가져오기를 수행할 수 있도록..
// rootMargin: '0px 0px 400px 0px'
})
io.observe(this.$refs.scrollObserver)
},
/**
* @summary 가져올 사진의 시작 포인트를 증가시킵니다.
*/
increasePhotoLength () {
this.length += 1
console.log(this.length)
}
}
})
View Compiled