[Vue.js] SPA로 동작하는 Vue에서 DOMContentLoaded
SPA 환경에서의 DOMContentLoaded 동작
SPA 환경에서는 아래와 같은 DOMContentLoaded 시점에 IntersectionObserver를 추가해주는 로직을 적용하는 과정이 의도한 것과 같이 동작하지 않는다.
document.addEventListener("DOMContentLoaded", function() {
var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;
lazyImage.classList.remove("lazy");
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazyImages.forEach(function(lazyImage) {
lazyImageObserver.observe(lazyImage);
});
} else {
// Possibly fall back to event handlers here
}
});
DOMContentLoaded는 초기 HTML 문서를 완전히 불러오고 분석했을 때 발생하는 이벤트이다. 모든 리소스가 다운된 다음 호출되는 load와는 달리, DOMContentLoaded는 StyleSheet, Image, 하위 프레임과 같은 리소스 로딩을 기다리지 않고 DOM트리가 완성되는 순간 즉시 호출된다.
SPA는 기본적으로 웹 애플리케이션에 필요한 모든 정적 리소스를 최초 접근 시 단 한번만 다운로드 한다. 이후 새로운 페이지 요청 시, 페이지 갱신에 필요한 데이터만을 JSON으로 전달받아 페이지를 갱신하므로 전체적인 트래픽을 감소시킬 수 있고, 전체 페이지를 다시 렌더링하지 않고 변경되는 부분만을 갱신하므로 새로고침이 발생하지 않아 네이티브 앱과 유사한 사용자 경험을 제공할 수 있다.
해결 방법
그렇다면 SPA로 동작하는 Vue와 Nuxt 환경에서는 위와 같은 코드를 어떻게 적용할 수 있을까?
라이프사이클(mounted) 훅 이용하기
export default {
data() {
return {
...
}
},
mounted () {
var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;
lazyImage.classList.remove("lazy");
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazyImages.forEach(function(lazyImage) {
lazyImageObserver.observe(lazyImage);
});
} else {
// Possibly fall back to event handlers here
}
}
mounted 라이프사이클 훅은 인스턴스가 마운트된 후 호출되며, 여기서 Vue.createApp({}).mount()로 전달된 엘리먼트는 새로 생성된 vm.$el로 대체된다. 루트 인스턴스가 문서 내의 엘리먼트에 마운트되어 있으면, mounted가 호출될 때 vm.$el도 문서에 포함(in-document)된다.
mounted는 모든 자식 컴포넌트가 마운트되었음을 보장하지 않으며, 전체 화면내용이 렌더링될 때까지 기다리려면, mounted 내에서 vm.$nextTick를 사용한다.
mounted를 이용한 방법은 해당 페이지마다 선언해줘야하는 번거로움이 있다.
DOMContentLoaded과 유사하게, 모든 페이지들이 호출되고 렌더링되는 시점마다 해당 로직을 수행해야한다면 어떻게 해결해야할까
Route가 변경되는 시점 이용
watch: {
$route () {
setTimeout(() => {
// ...
}, 50);
// react to route changes...
},
},
Nuxt를 사용 중이라면 App.vue, Default.vue와 같은 기본 레이아웃 컴포넌트에 watch 속성을 사용하여 route 값이 변경될 때마다 해당 로직을 수행하도록 수행해줄 수 있다. 다만, route가 변경되는 시점에는 아직 DOM이 업데이트된 상태가 아니므로 setTimeout과 같은 지연함수를 이용해 일정시간 딜레이가 필요하다.
하지만, 이 방법은 렌더링 시점을 보장하지 않으므로 로딩이 느린 상황에서는 정확성이 떨어진다.
Vue.mixin을 이용
믹스인(Mixins)은 여러 컴포넌트 간에 공통으로 사용하고 있는 로직, 기능들을 재사용하는 방법이다. 믹스인에 정의할 수 있는 재사용 로직은 data, methods, 라이프사이클 훅(created, mounted ...) 등과 같은 컴포넌트 옵션이다.
import Vue from 'vue';
const createLazyImageObserver = () => {
// ...
return lazyImageObserver;
} else {
// Possibly fall back to event handlers here
}
};
const isPageComponent = name => name.contains('[페이지를 구분할 수 있는 키]');
const mixin = {
data: () => ({
lazyImageObserver: null,
}),
mounted () {
if (isPageComponent(this._name)) {
this.lazyImageObserver = createLazyImageObserver();
}
},
destroyed () {
if (isPageComponent(this._name)) {
this.lazyImageObserver.disconnect();
}
},
};
Vue.mixin(mixin);
위와 같이 Mixin을 플러그인으로 설정하여, 전역으로 선언해주는 방법이 있다.
매 컴포넌트의 mounted 시점에서 해당 로직을 수행할 수 있는데, 만약 페이지 단위에서만 수행하고 싶다면 페이지를 구분할 수 있는 키를 name으로 설정하여 필터링하는 방법이 있다.
하지만, 전역으로 사용되는 Mixin은 모든 Vue 인스턴스에 영향을 미치므로 사용을 지양하고 있다.
따라서 코드의 중복을 피하고 싶다면 3번, 유지보수 방면에서 코드의 안정성을 추구한다면 1번을 추천할 수 있겠다.
Reference
https://v3.ko.vuejs.org/api/options-lifecycle-hooks.html#beforemount