<nav class="nav">
<a href="/" class="nav__link" data-link>Dashboard</a>
<a href="/posts" class="nav__link" data-link>Posts</a>
<a href="/settings" class="nav__link" data-link>Settings</a>
</nav>
이 영상에서 SPA를 구현하기 위해, a 태그를 클릭 했을 때 data-link 속성이 있는 경우 History API를 이용해 페이지를 이동하도록 구현한다.
// https://github.com/dcode-youtube/single-page-app-vanilla-js/blob/master/frontend/static/js/index.js#L54-L63
document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('click', (e) => {
if (e.target.matches('[data-link]')) {
e.preventDefault()
navigateTo(e.target.href)
}
})
router()
})
하지만, 다음과 같은 경우에는 예상과 다르게 동작했다.
<a href="/posts" class="nav__link" data-link>
<span>Posts</span>
</a>
이 경우, 이벤트 버블링으로 인해 e.target은 span이 되기 때문에 e.target.href는 undefined가 된다.
이벤트 버블링은 이벤트가 발생한 요소에서 시작해서 상위 요소로 전달되는 현상을 말한다.
그래서 e.target이 a 태그가 아닌 경우에는 e.target의 부모 노드를 찾아야 한다.
첫번째 방법: 부모 노드를 재귀적으로 찾기
target(Element) 안에 parentElement를 통해 부모 노드를 확인해서 a 태그가 나올 때까지 재귀적으로 찾는 방법을 생각해보았다.
하지만 재귀는 무한 루프에 빠질 수 있는 위험이 있기 때문에, 다른 방법이 있는지 찾아보았다.
두번째 방법: closest
Element 메서드 중 closest라는 메서드가 있다.
Element.closest() 메서드는 CSS selector를 인자로 받아서, 해당 selector와 일치하는 노드를 찾을 때까지 부모 노드를 탐색한다. 만약 일치하는 노드를 찾지 못하면 null을 반환한다.
이 메서드를 이용하면 위의 문제를 해결할 수 있다.
document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('click', (e) => {
const closestLink = e.target.closest('[data-link]')
if (closestLink) {
e.preventDefault()
navigateTo(closestLink.href)
}
})
router()
})
세번째 방법: pointer-events: none 사용하기
a 태그의 자식 요소를 클릭했을 때, 부모 요소의 이벤트를 막는 방법으로 pointer-events: none을 사용할 수 있다.
a > * {
pointer-events: none;
}
하지만, 만약 a 태그의 자식 요소에 클릭 이벤트를 추가하고 싶은 경우에는 이 방법을 사용할 수 없다. 그래서 이 방법은 추천하지 않는다.
결론
이 문제를 해결하기 위해, closest 메서드를 사용하는 것이 가장 좋은 방법이라고 생각한다.