Skip to content

컴포넌트 이벤트 디자인 하기

YongWoo Jeon edited this page Mar 4, 2021 · 1 revision

컴포넌트 이벤트 이름 작성 규칙

컴포넌트 이벤트 이름 작성 규칙은 컴포넌트를 만들때 컴포넌트 끼리 비슷한 규칙으로 만들어 사용자가 컴포넌트를 이질감 없이 사용하기 위해 작성했다. 기본 컨셉은 W3C의 DOM의 이벤트를 바탕으로 우리가 컴포넌트를 만든 경험을 추가했다.

이벤트에 stop 메서드는 v3 부터 기본으로 지원되지 않습니다. 링크를 참고하여 ComponentEvent를 사용해주세요.

beforeXXX 와 XXX 이벤트

두 이벤트를 구분하여 만든다.

: 컴포넌트의 이벤트를 만들 때 어떤 경우는 기능이 동작한 후에 발생해야하고, 어떤 경우는 동작 전에 이벤트가 발생해야 하는 경우가 있다. 예를 들어, 우리가 checkbox 컴포넌트를 만든다고 생각해보자. 그리고 해당 checkbox는 change라는 이벤트가 존재한다. change이벤트는 [ ]에서 [V]으로 변경된 후에 발생하는 이벤트이다. change이벤트는 보통 아래와 같이 변경된 상태를 서버에 전송하는등의 작업을 한다.

const checkbox = new CheckBox("#id");
checkbox.on("change", ({type})  => {
	fetch("/change",{
		body: JSON.stringify({type})
	}).then( e => {
		console.log("success");
	});
});

근데 [ ]에서 [V]으로 변경되기 전에 특정 조건인 경우를 확인 후에 change이벤트의 로직을 처리해야한다면 아래처럼 change이벤트에서 처리할 수도 있다.

checkbox.on("change", ({type})  => {
	if( condition ){
		fetch("/change",{
			body: JSON.stringify({type})
		}).then( e => {
			console.log("success");
		});
	} else {
		// [V] -> [ ]으로 변경하는 로직
	}
});

하지만, change이벤트는 이미 [ ]에서 [V]으로 변경됐기 때문에 다시 [V]에서 [ ]으로 돌려야 하는 로직이 추가되어야 한다. 그래서 change이벤트를 처음과 다르게 [ ]에서 [V]으로 변경되기 전에 발생하는 이벤트로 변경하자. 그러면 아래와 같이 되돌아 가는 로직이 필요없어진다. 물론, 초기의 로직과 다르게 상태가 변경되기 전이기 때문에 변경되었다고 감안하고 작성해야 한다.

checkbox.on("change", ({type})  => {
	if( condition ){
		type = type == 1 ? 2 : 1; // 아직 변경된 것이 아니기 때문에 1이라 2로 변경해야함.
		fetch("/change",{
			body: JSON.stringify({type})
		}).then( e => {
			console.log("success");
		});
	}
});

다음으로 파일 업로드를 개발하는데 파일을 업로드하고 발생하는 upload이벤트를 만들었다고 하자.

const fileUpload = new FileUpolad("#id");
fileUpload.on("upload", {files}  => {
	notifier(`${getFileCount(files)}개의 파일이 업로드되었습니다`);
});

이렇게 보면 Checkboxchange이벤트와 FileUploadupload이벤트는 이름만 가지고는 행위(file이 업로드)가 끝난 후에 발생하는 이벤트인지, 행위가 끝나기 전(checkbox의 변경)에 발생하는 이벤트인지 구분할 수가 없다. 그래서 사용자가 이벤트 명만 가지고 행위가 끝나고 발생하는 이벤트인지(upload), 행위가 발생하기 전 이벤트인지(change)을 구분할 수 있게 행위가 끝나기 전에 발생하는 이벤트는 before prefix을 사용한다.

egjs에서 만든 컴포넌트가 제공하는 beforeXXX이벤트는 행위가 동작하기 전에 발생하는 이벤트이고 XXX는 행위가 끝난 후 발생하는 이벤트이다. 예를들어, beforeFlickStartflickStart라는 이벤트를 시작하기 전에 동작하는 이벤트다. flickflick이란 행위가 끝나고 발생하는 이벤트다. 일반 이벤트 명은 after prefix가 붙어 있는 느낌으로 동작한다고 생각하면 이해하기 쉽다.

그리고 beforeXXXXXX는 컴포넌트의 특성상 쌍이 아닌 한쪽만 존재할 수 있지만, beforeXXX이벤트는 항상 XXX이벤트를 만들 준비가 되어 있어야 한다. 즉, 다음에 알아볼 beforeXXX이벤트에서는 stop메서드로 행위를 멈출 수 있어야 한다.

두 이벤트는 동기적으로 동작하며, stop으로 행위를 멈출 수 있다.

beforeXXX은 기본적으로 행동이 동작하기 전에 발생하고 XXX는 행위가 동작한 후에 발생한다. 앞에 예를 좀 더 정확한 방법으로 만들어 보자. Checkbox는 행위가 동작하기 전에 condition절을 판단하여 변경할지 말지 판단해야 한다. 그렇다면 아래와 같이 stop메서드를 활용하여 만들 수 있다.

const checkbox = new CheckBox("#id");
checkbox.on("beforeChange", ({stop})  => {
	if( !condition ){
		stop();
		// stop을 하면 Checkbox는 [ ] -> [V]으로 변경하지 않는다.
		// 상태가 변경되지 않았기 때문에 change 이벤트 역시 발생하지 않는다.
	}
});
checkbox.on("change", ({type})  => {
	fetch("/change",{
		body: JSON.stringify({type})
	}).then( e => {
		console.log("success");
	});
});

egjs는 Component클래스를 상속받아 컴포넌트를 만든다. 이렇게 만든 컴포넌트는 이벤트을 발생시킬때 등록된 이벤트 리스너의 첫번째 인자로 커스텀 Event객체를 전달한다. 커스텀 이벤트 객체에는 stop이란 특별한 메서드가 존재한다. stop이란 메서드를 사용하면 컴포넌트 내부에서는 행위([ ] -> [V]으로 변경할지,파일을 업로드할지..)를 동작하지 않게 해야 한다. 그렇기 때문에 stop을 사용하면 XXX이벤트는 발생하지 않는다.

이렇게 beforeXXXXXX는 순서적으로 발생하며, beforeXXXXXX을 멈출 수 있는 stop메서드를 사용할 수 있다. XXX에서도 stop메서드를 사용할 수 있지만, 이미 행위가 동작한 후이기 때문에 동작에 영향을 주지 못한다.

컴포넌트 개발자는 반드시 beforeXXX을 만들 때 사용자가 stop이벤트를 사용할 수 있음을 감안하고 개발해야 한다.

XXXStart와 XXXEnd 이벤트

진행되는 과정의 이벤트는 XXXStart ... XXXEnd으로 만든다.

changeupload처럼 행위가 하나인 경우도 있지만, 어떤 행위는 시작하고 끝이 있는 이밴트를 발생해야 할 경우가 있다. 이럴때, 시작하는 이벤트의 경우 XXXStart와 같이 postfix을 사용하고, 끝나는 시점에는 XXXEnd와 같이 postfix을 사용한다. 예를 들어, 애니메이션이 동작하는데 시작, 중간, 끝의 이벤트를 만들고 싶다고 가정하자. 그렇다면 animationStart, animationInterval, animationEnd와 같이 만들게 된다.

const animation = new SuperAnimation("#id");
animation.on("animationStart", (e)  => {
 	// animation 시작
});
animation.on("animationEnd", (e)  => {
 	// animation 끝
});

before~ 이벤트와 ~start 이벤트는 관련이 없다.

앞의 구조로 만들게 된다면, beforeXXXXXXStart의 관계가 헷갈리게 된다. 예를 들어, Drag컴포넌트를 만든다고 하자.

const drag = new Drag("#id");
drag.on("dragStart", (e)  => {
 	// drag 시작
});
drag.on("drag", (e)  => {
 	// drag 중
});
drag.on("dragEnd", (e)  => {
 	// drag 끝
});

위와 같이 drag가 시작하는 시점에는 dragStart, drag중에는 drag, drag 완료한 시점에는 dragEnd가 발생하도록 만들었다. 그리고 위의 checkbox와 유사하게 기본 동작인 drag가 시작하기 전에 drag을 동작하지 않게 하고 싶다고 가정하자. 그러면 dragStart시점에 e.stop()을 하면 drag가 시작하지 않아야 할 것 처럼 느껴지지만 그렇지 않다. dragStart는 이미 dragStart의 기본 기능이 실행된 이후에 발생하는 이벤트로 drag와는 상관없다. 만약에 drag을 멈추고 싶다면 dragStart가 아니라 drag의 기본 기능을 멈추는 beforeDrag을 만들어서 e.stop으로 drag을 멈추는게 규칙이다.

const drag = new Drag("#id");
drag.on("dragStart", (e)  => {
 	// drag 시작
});
drag.on("beforeDrag", ({stop})  => {
 	// drag 되기 직전
	stop();
});
drag.on("drag", (e)  => {
 	// drag 중
});
animation.on("dragEnd", (e)  => {
 	// drag 끝
});

즉, 간단하게 설명하면 이미 기본 기능이 동작한 후에 발생하는 XXX(dragStart, drag, dragEnd)는 커스텀 이벤트 객체의 stop으로 서로 간의 직접적으로 영향을 주지 않는다고 생각하면 된다. 기본 기능에 영향을 주는 이벤트는 beforeXXX이다.

~start 이벤트와 ~end 이벤트사이에 간접적으로 기본 기능에 영향을 줄 수 있다.

앞의 규칙으로 작성하면 다른 의문이 생긴다. 위에 예처럼 beforeDrag시점에 drag을 멈추도록 stop을 했다면, dragEnd는 발생해야 하는 것인가? 안 발생해야 하는 것인가? 이 부분을 규칙으로 정하려고 했지만, 컴포넌트마다 꼭 XXXEnd가 발생해야 하는 경우가 있고 XXXEnd가 발생하지 않거나 XXXAbort와 같이 다른 식으로 처리하거나, 크게 상관없는 경우가 있다. 그리고 이 케이스는 컴포넌트가 발젼되면서 바뀌기도 한다. 그래서 규칙은 직접적으로 영향을 주진 말고 beforeXXX을 통해서 간접적으로 영향을 주는 케이스는 컴포넌트 개발자의 재량이 맡기는게 좋겠다고 정리했다.

위의 Drag컴포넌트의 예를 들어보자.

const drag = new Drag("#id");
drag.on("dragStart", ({target})  => {
 	// drag 시작
	target.classList.add("selected"); // 시작할 때, selected클래스 추가
});
drag.on("beforeDrag", ({target, stop})  => {
 	// drag 되기 직전
	if( target.classList.contain("disabled"){ // 시작할 때, disabled클래스가 있으면 stop
		stop();  
	}
	
});
drag.on("dragEnd", ({target})  => { // 끝이면, selected클래스 삭제
 	// drag 끝
	e.target.classList.remove("selected");
});

위와 같이 작성했을 때 beforeDrag에서 stop을 할 경우에도 dragEnd가 발생하지 않는게 Drag컴포넌트에서는 적절해보인다.

const file = new FileUpload("#id");
file.on("uploadStart", ({target})  => {
 	// file 유효성 검증
});
file.on("uploadProgress", ({target, stop})  => {
	// 파일 업로드 중	
});
file.on("uploadEnd", ({target})  => {
 	// 파일 업로드 완료
	e.target.classList.remove("selected");
});
file.on("uploadAbort", ({target})  => {
 	// 파일 업로드 중단
	e.target.classList.add("abort");
});

위의 경우는 반대로 uploadEnd을 발생하지 않고 uploadAbort로 처리하는게 적절하다. 그래서 이렇게 통일화된 규칙을 만들기 어렵기 때문에 개발자가 고민해서 결정하기로 정했다.