디자인으로 변형된 표의 문서 구조 돌려놓기

이상현·2022년 7월 7일
0

document design

목록 보기
1/1

misson

최근에 받았던 디자인으로, 두 가지 서로 다른 옵션에서의 스펙을 약간의 애니메이션이 가미된 화면으로 보여주는 게 있었다.

가운데에 A, B 옵션 이름이 있고, 그 좌우로 스펙의 값이 있고, 값 크기만큼 그래프를 가로로 채우는 디자인인데, 문서 상으로는 표 - table - 에 맞아 보였다.

문서로 보면 아래와 같을 것이다.

항목AABB
입력값100100
실제 출력값10050
손실율0%50%
// table markup
<table>
	<thead>
    	<tr>
        	<th scope="col">항목</th>
            <th scope="col">AA</th>
            <th scope="col">BB</th>
        </tr>
    </thead>
    <tbody>
    	<tr>
        	<th scope="row">입력값</th>
            <td>100</td>
            <td>100</td>
            
        </tr>
        <tr>
        	<th scope="row">실제 출력값</th>
        	<td>100</td>
            <td>50</td>
        </tr>
        <tr>
         	<th scope="row">손실율</th>
        	<td>0%</td>
            <td>50%</td>
        </tr>
    </tbody>
</table>

issue

표의 특징은 상단과 왼쪽 맨 앞에 주제어가 온단 점이다. 디자인에 맞추려면 표의 순서를 조정해 줘야 했는데, grid의 grid-area나 flex의 order를 이용하면 가능할 것으로 보였다. 다만 길은 있어도 가기가 주저됐었는데, 요컨대 table 마크업은 tag의 native sementic에는 유리하지만 table display를 비롯해 user agent style을 덮어쓰며 교정하는 부담이 오는 작성법이고, 특히 table display를 수정하는 것이 html의 중첩 수준만큼 부담이 늘어날 것으로 예상되었다.

피할 수 있는 길이 있을까.

thiking point

// table
<tr>
  <th scope="col">항목</th>
  <th scope="col">AA</th>
  <th scope="col">BB</th>
</tr>
<tr>
  <th scope="row">입력값</th>
  <td>100</td>
  <td>100</td>
</tr>

// non sementic
<div>
  <div>항목</div>
  <div>AA</div>
  <div>BB</div>
</div>
<div>
  <div>입력값</div>
  <div>100</div>
  <div>100</div>
</div>

두 마크업의 차이는 정보들 사이의 관계 여부이다.

단순히 요소 자체가 의미가 있는지 여부와 별개로 정보들 사이의 관계성이 있는지를 확인해보면, 사람의 사고에는 조합할 수 있는 능력이 있으니 이렇게 한눈에 들어오는 구조라면 table이 아니라도 정보의 반복되는 규칙성으로부터 관계도 알아볼 수 있을 것이다.

"항목 AA BB 입력값 100 100"

을 나열해놓고 어디선가 끊어읽기를 하면 뒤에 오는 정보를 앞에 대입해 볼 수 있지 않을까. 이 이해에선 table 마크업와 같은 관계를 읽어낼 수 있을 것이다. 예를 들어 (항목 AA BB)(입력값 100 100)으로 괄호를 쳐줄 수 있는 무언가가 있다고 하면 괄호로 그루핑이 된 정보 안의 순서 - index - 로 관계성을 알아보는 것이다.

<ul>
  <li>항목</li>
  <li>AA</li>
  <li>BB</li>
</ul>
<ul>
  <li>입력값</li>
  <li>100</li>
  <li>100</li>
</ul>

이렇게 말이다.

실제로 표를 table이 아닌 list로 마크업을 한 기록지를 본 적이 있다. 지금은 서비스 리뉴얼로 흔적을 찾아볼 수 없지만, 과거에 nba.com에서 선수들의 기록지를 그렇게 만들었다. 열을 하나의 목록으로 보고 나열하는 정보의 순서 - index - 로 서로 다른 목록 간에 정보가 매칭이 된다는 전제 아래서 만든 것일 거다.

그런데 그 기록지는 3x4가 아니라 수십x수십 이상의 내용이었다. 실제로는 수백x수십의 내용이다. 선수들이 많아서 정보를 끊기 위해 pagination을 달아 한번에 보는 목록의 수가 수십이었을 뿐이었다. 그럼 이런 문서 구조에서 알아볼 수 있을까. 374번째 선수의 8번째 정보를.

무엇보다 이런 이해는 설명을 더해가며 그렇게 볼 수 있다 여긴 것이라 명시적인 내용을 보고 이해한 것은 아니다. 접근성은 암시적이거나 묵시적인 정보 제공을 피할수록 좋았다.

결론은, table은 table로 만들어야 했다. 원점으로 돌아갔다.

Idea

그럼 반대로 sementic이 없는 단순 division element를 어떻게 의미론적으로 "표"로 만들 수 있을까. 거기에 길을 제시해 준 것이 aria role(from wai-aria)이다.

<nav role="table" class="gnb">...</nav>

<div role="navigation" class="gnb"></div>

html의 native sementic으로 위쪽은 navigation이다. 그렇지만 접근성 보조 기술 도구에서 저 요소는 table이다. 반대로 아래는 html로 보면 단순하 division이지만, 접근성 보조 기술 도구에서는 <nav> 와 같다.

wai-aria는 어떤 태그로 어떻게 만들었든 native sementic과 문서의 구조를 그 바닥부터 완전히 바꿀 수 있다. 그래서 wai-area 명세의 인트로*에서 이런 부분 - 본래의 의미와 구조를 다 바꿔버릴 수 있음 - 을 유의하며 작성할 것을 주문하고 있다.
* Authors can inadvertently override accessibility semantics.

implement

우선 레이아웃에 맞는 마크업을 작성하면 아래와 같았다. table과 직접 비교 및 css를 생략하기 위해 classname으로 구조 표현을 대신한다.
*문서 구조 | 스타일 | 그 외... 순.

// layout markup
<div class="table">
	<div class="table-row | flex-container | for-screen-reader">
    	<div class="cell head-cell | flex-item">항목</div>
    	<div class="cell head-cell | flex-item">AA</div>
    	<div class="cell head-cell | flex-item">BB</div>
    </div>
    <div class="table-row | flex-container">
    	<div class="cell head-cell | flex-item flex-order__3">입력값</div>
        <div class="cell | flex-item">100</div>
        <div class="cell | flex-item flex-basis__1 animation__graph" data-graph-value="100" aria-hidden="true"></div>
        <div class="cell | flex-item flex-basis__1 flex-order__4 animation__graph" data-graph-value="100" aria-hidden="true"></div>
        <div class="cell | flex-item flex-order__5">100</div>
    </div>
    <div class="table-row | flex-container">
        <div class="cell head-cell | flex-item flex-order__3">실제 출력값</div>
        <div class="cell | flex-item">100</div>
        <div class="cell | flex-item flex-basis__1 animation__graph" data-graph-value="100" aria-hidden="true"></div>
        <div class="cell | flex-item flex-basis__1 flex-order__4 animation__graph" data-graph-value="50" aria-hidden="true"></div>
        <div class="cell | flex-item flex-order__5">50</div>
    </div>
    <div class="table-row | flex-container">
    	<div class="cell head-cell | flex-item flex-order__3">손실율</div>
    	<div class="cell | flex-item">0%</div>
        <div class="cell | flex-item flex-basis__1" aria-hidden="true"></div>
        <div class="cell | flex-item flex-basis__1 flex-order__4" aria-hidden="true"></div>
    	<div class="cell | flex-item flex-order__5">50%</div>
    </div>
</div>

표의 순서는 지키면서 css로 위치만 조정하면 될 마크업이지만, table과 같은 정보들 간의 관계가 없다. 이 부분을 잡아주는데 wai-aria가 들어올 수 있다.

사용할 속성은,
sementic을 부여할 role,
표의 행과 열의 순서를 지정해 줄 - scope 속성을 대신할 - aria-colindex(aria-rowindex),
그리고 이 표의 행렬 사이즈를 알려줄 aria-colcount(aria-rowcount)이다.

// replace table markup
<div role="table" aria-colcount="3" aria-rowcount="4">
	<div role="row" class="flex-container | for-screen-reader" aria-rowindex="1">
    	<div role="columnheader" class="flex-item" aria-colindex="1">항목</div>
    	<div role="columnheader" class="flex-item" aria-colindex="2">AA</div>
    	<div role="columnheader" class="flex-item" aria-colindex="3">BB</div>
    </div>
    <div role="row" class="flex-container" aria-rowindex="2">
    	<div role="cell" class="flex-item flex-order__3" aria-colindex="1">입력값</div>
        <div role="cell" class="flex-item" aria-colindex="2">100</div>
        <div class="flex-item flex-basis__1 animation__graph" data-graph-value="100" aria-hidden="true"></div>
        <div class="flex-item flex-basis__1 flex-order__4 animation__graph" data-graph-value="100" aria-hidden="true"></div>
        <div role="cell" class="flex-item flex-order__5" aria-colindex="3">100</div>
    </div>
    <div role="row" class="flex-container" aria-rowindex="3">
        <div role="cell" class="flex-item flex-order__3" aria-colindex="1">실제 출력값</div>
        <div role="cell" class="flex-item" aria-colindex="2">100</div>
        <div class="flex-item flex-basis__1 animation__graph" data-graph-value="100" aria-hidden="true"></div>
        <div class="flex-item flex-basis__1 flex-order__4 animation__graph" data-graph-value="50" aria-hidden="true"></div>
        <div role="cell" class="flex-item flex-order__5" aria-colindex="3">50</div>
    </div>
    <div role="row" class="flex-container" aria-rowindex="4">
    	<div role="cell" class="flex-item flex-order__3" aria-colindex="1">손실율</div>
    	<div role="cell" class="flex-item" aria-colindex="2">0%</div>
        <div class="flex-item flex-basis__1" aria-hidden="true"></div>
        <div class="flex-item flex-basis__1 flex-order__4" aria-hidden="true"></div>
    	<div role="cell" class="flex-item flex-order__5" aria-colindex="3">50%</div>
    </div>
</div>

문서 구조의 관점에서 가로의 막대 그래는 실제로는 cell이 아니다. 개발자의 입장에서 보면 dummy일 거다. 디자이너의 입장에서는 UX의 포인트일텐데 말이다.

구조에 표현을 입힐 때는 되도록 생애주기(lifecycle)가 가장 긴 것에 가장 오래도록 남을, 또 가장 기본적인 선언을 하는 게 유리하다고 한다. aria 속성은 css의 속성 선택자의 대상이면서 이 레이아웃을 사용하는 한 lifecycle이 가장 길 것이었다. 이 속성에 의존성을 가져도 무방했다. 그래서 실제 필요한 선언만 남기고 css를 aria 속성에 종속시키면,

// final
// markup
<div role="table" aria-colcount="3" aria-rowcount="4">
	<div role="row" aria-rowindex="1">
    	<div role="columnheader" aria-colindex="1">항목</div>
    	<div role="columnheader" aria-colindex="2">AA</div>
    	<div role="columnheader" aria-colindex="3">BB</div>
    </div>
    <div role="row" aria-rowindex="2">
    	<div role="cell" aria-colindex="1">입력값</div>
        <div role="cell" aria-colindex="2">100</div>
        <div data-graph-value="100" aria-hidden="true"></div>
        <div data-graph-value="100" aria-hidden="true"></div>
        <div role="cell" aria-colindex="3">100</div>
    </div>
    <div role="row" class="flex-container" aria-rowindex="3">
        <div role="cell" aria-colindex="1">실제 출력값</div>
        <div role="cell" aria-colindex="2">100</div>
        <div data-graph-value="100" aria-hidden="true"></div>
        <div data-graph-value="50" aria-hidden="true"></div>
        <div role="cell" aria-colindex="3">50</div>
    </div>
    <div role="row" class="flex-container" aria-rowindex="4">
    	<div role="cell" aria-colindex="1">손실율</div>
    	<div role="cell" aria-colindex="2">0%</div>
        <div aria-hidden="true"></div>
        <div aria-hidden="true"></div>
    	<div role="cell" aria-colindex="3">50%</div>
    </div>
</div>
// scss
// -=-= aria table =-=-
$table-header : [aria-rowindex="1"];
$table-row : [role="row"];
$row-head-cell : &:not([aria-rowindex="1"]) [aria-colindex="1"];
$animation-cell : [data-graph-value*="0"][aria-hidden="true"];

#{$table-header} {
    width:1px;
    height:1px;
    position:relative;
    overflow:hidden;
    clip:rect(0,0,0,0);
}
#{$table-row} {
	display:flex;
    #{$row-head-cell} {
      	order:3;
        & ~ :nth-last-child(2) {
           order:4;
        }
        & ~ :nth-last-child(1) {
           order:5;
        }
    }
    #{$animation-cell} {
		flex:1;
	}
}
// -=-=-=- end

nvba 스크린리더로 보면 정보 관계가 본래의 표와 같이 설정된 것을 확인할 수 있다.

ps.
table / cell role은 clickable 요소가 포함된 경우 grid / gridcell로 바꿀 수 있다.
click할 요소가 있는 테이블 ui로는 datepicker가 있는데, 예약 서비스를 직접 만들지 않으면 보통 3rd party 라이브러리를 사용한다. 다만 이때에도 마크업을 table로 제공해주는 걸 찾아서 사용하는 것이 좋다. html의 native 기능과 의미를 따르는 게 가장 안전하므로. (이 이야기는 언젠가 또... 일단은 live sample을 남겨본다.)

profile
이런 건 왜...

0개의 댓글