이벤트 프로그래밍에 Flux pattern 써보기

이상현·2022년 7월 9일
0

memorize

목록 보기
1/2

접근성 이슈 중의 하나로 해결하기 쉽지 않고 ui 디자인도 어려운 것이,
바로 탭 - Tab - 이다.

  1. 탭 메뉴의 이름은 길이를 산정할 수 없고,
  2. 화면의 너비는 사용자의 접속 기기에 따라 다르며,
  3. 이로 인해 화면에서 잘리는 탭 메뉴 이름을 보기 위해 가로 스크롤을 제공한다면 동시에 현재 선택한 탭 메뉴를 전후로 오가거나 현재의 스크롤 위치를 옮겨갈 수 있는 단일 수행 interface - arrow - 가 필요하다.
  4. 스크롤은 탭 메뉴를 탐색만 하며, 메뉴 선택은 click 또는 touch로만 일어난다.
  5. 현재 선택한 탭 메뉴가 화면에 표시되어야 한다.
  6. 키보드 제어 시, 초점의 움직임과 탭 메뉴 선택을 분리 - manual select - 하는 것이 기본이지만 이 두가지가 같이 일어날 수도 있다. ( 즉, active과 :focus 디자인을 분리하는 기준이 없다. 우열관계가 필요한 것. )
  7. manual select 모드에서는, 선택한 탭 메뉴 이외에 다른 탭 메뉴는 focusable 요소가 아니다. tab key가 아닌 arrow key로 옮겨가야 한다.
  8. 글로벌 서비스인 경우엔 언어가 오른쪽에서 왼쪽으로 진행되는 아랍, 히브리어 계를 고려해야 한다. 그러니까 동작 방식은 같지만, 운동? 진행? 방향은 반대로 가야 한다.

일반적인 이벤트 프로그래밍으로 위 내용을 요약하면,

keyup : 메뉴 선택
keydown : 메뉴 전후 이동 | 스크롤 위치 이동
wheel scroll : 스크롤 위치 이동
click : 메뉴 선택
touch : 메뉴 선택 | 메뉴 전후 이동 | 스크롤 위치 이동
mouse : 메뉴 선택 | 메뉴 전후 이동 | 스크롤 위치 이동

그리고 기본 선택 메뉴가 첫번째가 아니라면 선택 메뉴의 index를 찾아서 선택 표시와 스크롤이 이동하는 load 이벤트도 필요하다.

정의된 inteaction의 내용이 이와 같은 터라, 이벤트 타입을 기준으로 하건 사용자 입력을 받는 ui 요소를 기준으로 하건 중복 코드가 생기는 것을 막기 어렵고 단일 책임 원칙을 지키기도 어렵다. 메뉴 선택의 수행 모듈을 만들면 4가지 이벤트 리스너에 코드를 넣어줘야 하는데, 다른 수행 패턴도 정의되어 있기 때문에 수행 모듈 안에 코드가 이중으로 동작하는 것을 막으려면 모듈을 쪼개야 하기 때문에 실제로는 규칙이랄 게 없다.

ui 패턴의 접근성 적용을 위해 wcag가 직접 접근성을 충족하는 패턴을 기본 ui들에 대해 제공하고 있지만, 탭 ui의 메뉴들에 스크롤이 달려있고, 그걸 옮겨다닐 수 있는 동작까지 포함하고 있진 않다. 정말 기본적인 수행에 충실한 내용이다.

스파게티 코드가 예정되어 보였는데...

이때가 한창 react-redux를 적용하고 있던 때이고, 혹여 나중에 라이브러리를 만든 걸 쓴다면 react에서도 쓸 수 있으면 좋겠다고 생각해서 떠오른 아이디어가 action이었다.

사용자의 행위를 click이나 scroll로 인지하지 않고,
scroll left
scroll right
select tab
...
등으로 인지하면, ui 요소는 단지 입력창과 같았다.

만약 prev 버튼을 click 했다면,

prev.addEventListener("click", ()=>{
	const active_index = find_active_index(tabmenu);
  	return dispatch({
		action : active_index === 0 ? "steady" : "select prev tab",
    	payload : active_index,
    });
});

이처럼 해당 동작을 전달만 하면 된다.

이렇게 하고 나니, arrow 버튼이 prev, next로 될 수도 있었고, scroll 위치를 이동하는 scroll left, scroll right로 될 수도 있었다. 구분은 - html 작성만으로 configuration을 하도록 - element에 data 속성으로 했고, 한번에 움직이는 거리도 data 속성으로부터 받았다. 위에서 언근한 8로 인한 이슈도 이 방법론에서는 간결하게 해결되었다.

prev.addEventListener("click", ()=>{
	const active_index = useStore(active_index);
  	const action = (({ mode })=>{
      	// no exist prev item
      	if(0 === active_index) 
        return "steady";
      	// mode : scroll
      	const isRTL = "rtl" === document.documentElement.getAttribute("dir") // ltr or rtl
      	if("scroll" === mode) 
        return `${mode} ${isRTL ? "right" : "left"}`;
      	// mode : select
      	return "select prev tab";
    })(this.dataset);
  	return dispatch({
    	action,
      	paylaod : active_index += -1,
    });
});

키보드 제어도 keycode에 따라 전달하는 action만 달라질 뿐 진행은 같았다.

store에선 전달받은 payload와 저장된 값만 비교하고 이후 처리는 controller에서 맡겼다. 이걸 만든 게 좀 예전이라 정확히 기억이 나지 않지만, 아마도 stream 개념을 코드로 만들 능력이 없었기 때문에 middleware를 둘 수 없었고, 그래서 뭔가 일이 생기면 다 controller로 보내버리려는 의도였던 것 같다. 지금에서 보면 store에 문을 두들기기 전에 해야 할 일을 일단 문을 열어 제껴서 떡밥 째로 던진 뒤에 그 떡밥을 store 뒷문에서 분해하는 모습이다. 그래서 controller에 온갖 동작 수행 모듈이 들어가고, 각 모듈마다 복잡한 계산식을 가지는 등 여러 문제가 있었지만 그걸 간략하게 줄이면 결국은 아래의 switch case문이다.

function controller(action){
	switch(action){
      	case "select tab" :
      	case "select prev tab" :
      	case "select next tab" :
        return select_menu(action.payload);
      	case "scroll left" :
      	case "scroll right" :
    	return move_scroll(action.payload);	// 이 때의 payload는 x축 position 값.
        default : 
        return "steady"
    }
}

...epilogue

이 작업을 하면서 처음으로 디자인 패턴을 깊게 고민한 것 같다. 이보다 앞서서 감시자, 팩토리 패턴 정도는 쓰고 있었는데 Flux는 data flow뿐 아니라 동작의 진행도 고려한 착상 - achitecture - 이었기 때문에 고민의 주제가 한두가지가 아니었고, 코드의 규모도 더 컸다. 의존성 관리를 active index로 몰기 위해 모든 모듈 함수의 매개변수를 index로 통일하고, 위치 계산등도 여기에 종속시켰던 기억이 있다.

나중에 Tab 안에 Tab을 만들 수 있도록 하고 한 페이지가 중복으로 사용하는 경우를 위해 결국 추상화 클래스가 필요했는데, esm을 기반으로 만들다보니 역설적으로 전역 변수의 존재가 거꾸로 반드시 필요하다는 걸 알았다.

렌더링 이후의 동적 제어를 위해 인스턴스 - 각각의 tab 객체 - 에 들어갈 수 있는 방법이 없었고, 데이터를 기준으로 템플릿에 컨텐츠를 넣는 게 아닌 템플릿의 컨텐츠를 제어하기 위해서 데이터를 사용하는 경우는 사고 방식도 거기에 맞춰야 한 것이다. 어찌보면 dom script의 방법론에서 오는 구조적인 필요성이었을 것이다. 종속 관계가 data > dom이 아닌 dom > data이다 보니. active index도 결국 현재 렌더링 되어있는 dom tree의 순서를 주워쓰는 것이었으니까.


ps.
Tab은 그루핑한 컨텐츠를 모아보고 싶은데 스크롤로 처리하기에는 집중도 - 사용자의 집중도, 주제에 대한 집중도 - 가 떨어질 때 사용한다.

가령, 프로모션 제품이 20개가 있는데 TV, 모니터, 스마트폰등으로 제품을 명확하게 분류하고 싶다면 Tab을 사용할 수 있다. 혹은 TV 제품이 20개 있어 프로모션하는 제품과 그렇지 않은 제품, 또 신상품으로 categorizing을 할 때에도 사용할 수 있다. 이보다 더 많고 상세한 categorizing을 하고 싶을 때 등장하는 빌런이 Filter이다. DB 테이블을 폭발적으로 증가시키는 것도 얘일 거다.

profile
이런 건 왜...

0개의 댓글