프로젝트를 진행하면서, 여러 이미지들을 슬라이드해서 볼 수 있는 캐러셀을 직접 구현해보았다. 여기에 추가적인 기능으로, 캐러셀을 사용자가 선택하면 캐러셀을 화면에서 지우거나 편집하는 등의 메뉴를 볼 수 있는 기능이 제공된다.
구현하면서 캐러셀의 슬라이드 및 선택 상호작용이 함께 발생하는 문제를 식별했다.
캐러셀을 슬라이드할 경우 사용자는 이미지가 슬라이드되는 결과만을 기대할텐데, 요소가 동시에 선택되어 메뉴가 보이게되면서 기대하지 않은 동작으로 인해 사용자 경험이 떨어진다는 느낌을 받았다.
원인과 해결방안을 함께 살펴보자.
아래와 같은 코드로 작성될 경우 문제가 발생했다.
<!DOCTYPE html>
<html>
<head></head>
<body>
<my-wrapper>
<my-carousel></my-carousel>
</my-wrapper>
</body>
<script>
customElements.define('my-wrapper', class extends HTMLElement {
constructor() {
this.addEventListener('click', this.handleClick)
}
handleClick() {
// 선택된 요소 표시
}
})
customElements.define('my-carousel', class extends HTMLElement {
constructor()
this.addEventListener('mousedown', this.handleMouseDown)
this.addEventListener('mouseup', this.handleMouseUp)
}
handleMouseDown() {
// 슬라이드 시작 좌표 기록
}
handleMouseUp() {
// 슬라이드 끝 좌표와 시작 좌표를 계산하여 슬라이딩 처리
}
})
</script>
</html>
이 문제 원인은 이벤트 버블링으로 정의할 수 있다. 기본적으로 대부분의 이벤트들은 이벤트 버블링이 발생해, 발생한 이벤트들은 발생 최초 지점부터 시작해 html의 최상위 요소로 전파가 된다.
carousel을 슬라이드하기 위해서 마우스를 눌렀다가 조금 이동시키고 다시 떼는 상황을 떠올려보자.
(위 코드를 기반으로) 이 흐름에서는 3개의 이벤트 핸들러 모두 동작한다. click이벤트는 mouseup, mousedown이 차례대로 발생한 후 뒤따라 무조건 발생하는데, 이 이벤트에 대한 핸들러를 마침 상위 요소인 wrapper가 가지고 있기 때문이다.
이와 같이 이벤트 버블링이 발생함에 따라, 상위 wrapper 요소의 click 핸들러가 호출되게된다.
vanilla js 기반 프로젝트 개발환경에서 직접 구현한 전역 상태 관리 시스템을 이용하는 방식이다.
(변경된 코드 부분만 작성한 점을 참고)
customElements.define('my-wrapper', class extends HTMLElement {
// ...
handleClick() {
if (store.get('sliding_state')) {
store.set('sliding_state', false) // 상태 초기화
return
}
// 선택된 요소 표시
}
})
customElements.define('my-carousel', class extends HTMLElement {
// ...
handleMouseUp() {
if (diffX === 0) {
store.set('sliding_state', false)
return
}
store.set('sliding_state', true)
// 슬라이드 끝 좌표와 시작 좌표를 계산하여 슬라이딩 처리
}
})
stopPropagation()
customElements.define('my-carousel', class extends HTMLElement {
constructor()
this.addEventListener('mousedown', this.handleMouseDown)
this.addEventListener('mouseup', this.handleMouseUp)
this.addEventListener('click', this.handleClick)
}
handleMouseDown() {
// 슬라이드 시작 좌표 기록
}
handleMouseUp() {
// 슬라이드 끝 좌표와 시작 좌표를 계산하여 슬라이딩 처리
}
handleClick(event) {
if (diffX === 0) {
return
}
event.stopPropagation()
}
})
stopPropagation()
을 호출하는 것도 괜찮은 방법일 수 있다.new CustomEvent(type[, options])
과 같이 선언한다bubbles
, cancelable
과 같은 멤버변수에 접근가능함.bubbles
기본값이 false이기 때문에 버블링을 허용하려면bubbles: true
를 설정해야함.customElements.define('my-wrapper', class extends HTMLElement {
// ...
constructor () {
let isSlide = false
this.addEventListener('slide', () => {
isSlide = true
})
this.addEventListener('click', () => {
if (isSlide)
isSlide = false
return
}
// 선택된 요소 변경
})
}
}
customElements.define('my-carousel', class extends HTMLElement {
// ...
handleMouseUp() {
if (diffX === 0) {
return
}
const slideEvent = new CustomEvent('slide', { bubbles: true })
event.target?.dispatchEvent(slideEvent)
}
}
최종적으로 적용한 방식은 이벤트 전파를 중지하는 방식이었다.
전역 상태를 활용할 경우 의존 로직이 많고, CustomEvent 방안의 경우 stopPropagation()
방안에 비하여 로직이 복잡했기 때문이다.
만약 모든 이벤트들을 수신해야하는 것이 원칙이었다면 CustomEvent를 활용하는 방식을 선택했을 것이다.
https://ko.javascript.info/bubbling-and-capturing
https://developer.mozilla.org/ko/docs/Web/API/CustomEvent