📎 placeholder 사용에 관하여
첫번째는 placeholder 사용하는 것을 가급적 피하자는 것. assistive techonology를 사용하는 시각장애인들은 input 요소가 이미 작성된 것으로 착각하고 넘어갈 가능성이 존재함.
두번째는 사용하더라도 WCAG에서 제시하는 color contrast인 4.5:1의 비율을 준수할 것.
세번째는 label 대용으로 사용하지 말것. 입력하는 순간 사라지는 placeholder는 해당 input 요소가 무엇에 대한 정보를 입력하는지 알 수 없게됨. 굳이 사용해야 한다면 floating label UI로 만들자.
네번째는 설명과 관련한 내용을 placeholder로 사용하지 말것. 입력하는 순간 사라지 는 placeholder는 해당 input 요소가 어떤 것을 설명하고 있는지 알 수 없게됨. 설명은 별도의 태그로 분리하고
aria-describedby
를 이용하자// 올바르지 않은 <label for=”password1”>Password</label> <input type=”text” id=”password1” placeholder=”Password should be 8 characters with one number, one special character”> // 올바른 <label for=”password”>Password</label> <input type=”text” id=”password” placeholder="enter password" aria-describedby=”password-hint”> <span id=”password-hint”>Password should be 8characters long with a number & a special character</span>
The Anatomy of Accessible Forms: The Problem with Placeholders
📎 required 요소로 만들기
required attribute 대신 aria-required를 이용하자
A Guide To Accessible Form Validation
📎 label에 required mark (*) 삽입하기
aria-hidden은 focus mode에서 문제가되고, NVDA에서는 hidden을 무시하는 이슈가 있다. 결국에는 img 태그에 alt를 empty string으로 두고 쓰는 것이 좋다고 제시한다. 참고로 alt=""로 두는 것과 alt를 아얘 기제하지 않는 것은 다르다.
when-should-i-use-a-null-or-empty-alt-tag
📎 aria-hidden와 tabbing focus
aria-hidden은 accessibility tree에서 요소를 제거하는 것이지 tabbing focus는 여전히 동작한다. 그러므로 aria-hidden을 사용하는 경우 tabbing focus가 동작하지 않도록 만들어야한다.
📎 이외에 참고할만한
인터넷에 존재하는 토글 버튼 구현 관련 문서들을 보면 input, label 태그를 이용하여 구현하는 경우도 있고, 단순히 button 태그만을 이용해서 구현한 경우도 존재합니다. 둘의 차이점이 무엇인지 궁금하여 조사하게 되었고, 이 과정에서 알게된 wai-aria에 대해서 정리합니다.
📎 input과 label 태그를 이용하여 On/Off 스위치 만들기
다음과 같이 input 태그의 타입을 checkbox로하여 토글 버튼 구현이 가능하다.
<input type="checkbox" id="notify" name="notify" value="on"> <label for="notify">Notify by email</label>
위 요소를 포커싱하게 되는 경우 스크린 리더는
Notify by email, checkbox, unchecked
를 안내한다. 그리고 checkbox를 체크하는 경우, 스크린 리더는checked
를 안내한다. 위 코드는자바스크립트 없이 상태를 처리하고 스크린 리더가 유저에게 피드백
할수있다.만약 유저에게 조금 더 구체적으로 안내해야하는 경우 다음과 같이 작성할 수 있다.
<fieldset> <legend>Notify by email</legend> <input type="radio" id="notify-on" name="notify" value="on" checked> <label for="notify-on">on</label> <input type="radio" id="notify-off" name="notify" value="off"> <label for="notify-off">off</label> </fieldset>
legend인
Notify by email
은 컨트롤 라벨의 앞에 오게되는데, 가령 윈도우 스크린 리더인 JVDA는 grouping이라는 단어까지 함께 포함하여Notify by email, grouping, on radio button, checked, one of two
를 안내한다.checkbox를 이용하든 radio 버튼을 이용하든 모두 on/off 스위치로 동작 가능하고, 마우스, 키보드, 터치, 스크린 리더 등의 소프트웨어로 각기 다른 디바이스, 브라우저, OS등을 통해 이용이 가능하다.
하지만 form 요소는 아주 오랫동안 데이터를 수집하는 용도로 사용됐다. 그렇기에 유저는 checkbox나 radio를 이용하는것이 form의 제출로 동작할 것을 의심할 수 있다. 그러니까
checkbox나 radio 버튼은 데이터를 제출하는 input 요소로의 동작을 예상하기 때문에
어원이 문제가 될 수 있다는 뜻이다.그러므로
checkbox나 radio는 유저가 데이터를 제출하는 상황이 아니라는 것을 인지할 수 있을 때
만 사용하는 것이 좋다.📎 button 태그를 이용한 On/Off 스위치
웹 접근성을 제공하기 위해 WAI-ARIA 속성인
aria-pressed
를 이용할 수 있다.<button type="button" aria-pressed="true"> Notify by email </button>
WAI-ARIA는 아래에서 더 알아도록 하겠습니다. 버튼을 클릭하는 경우 다음과 같이 javascript를 이용하여 aria-pressed 상태를 바꿀 수 있습니다.
const toggle = document.querySelector('[aria-pressed]'); toggle.addEventListener('click', (e) => { let pressed = e.target.getAttribute('aria-pressed') === 'true'; e.target.setAttribute('aria-pressed', String(!pressed)); });
버튼에
aria-pressed
가 존재하는 경우, 스크린 리더는 이 버튼을toggle button
혹은push button
으로 인식한다. WAI-ARIA가 button의 정체성에 영향을 끼치는 것이다.만약
aria-pressed="true"
인 버튼에 focus가 되면 NVDA 스크린 리더는Notify by email, toggle button, pressed
를 안내한다. 그리고 버튼을 클릭하는 순간not pressed
를 즉각적으로 피드백한다.이러한 버튼 요소를 만드는 것에 있어서 주의해야 할점이 있다. 첫번째는
버튼이 무엇을 컨트롤하는지 라벨에 명시
해야 한다. 만약 라벨이 on, off / active, inactive / play, pause 라면 유저 입장에서는 어떤 것을 컨트롤하는지 알기 어렵다.두번째는
상태의 변경을 라벨로 드러내지 않는 것
이다. 예를들면 다음과 같이 label을 통해 상태를 드러내는 경우const button = document.querySelector('button'); button.addEventListener('click', (e) => { let text = e.target.textContent === 'Play' ? 'Pause' : 'Play'; e.target.textContent = text; });
스크린 리더는 상태가 변하는 순간 유저에게 피드백 할수 없게된다. 현재 버튼이 어떤 상태인지 확인하려면 unfocus 상태로 만들었다가 다시 focus 해야한다.
📎 보조 라벨링
만약 버튼에 이미지 symobl을 사용하는 경우 유의미한 accessible name으로 동작하지 않는다. 이때는 aria-label을 이용할 수 있다.
<!-- Paused state --> <button type="button" aria-pressed="false" aria-label="play"> ▶ </button> <-- Playing state --> <button type="button" aria-pressed="true" aria-label="play"> ⏸ </button>
aria-label은 유니코드 텍스트를 덮어씌우며 paused 버튼의 경우
play button, not pressed
와 같이 안내하고, play 버튼의 경우play button, pressed
를 안내한다.📎 label과 input 태그 배치
implicit label과 explicit label 두가지 경우로 배치가 가능하다.
implicit label을 사용하는 경우는 label 태그에 for attribute를 사용할 필요가 없다.
<label> Name: <input type="text" name="name" /> </label>
explicit label을 사용하는 경우는 label 태그에 for attribute를 사용한다.
<label for="name">Name: </label> <input type="text" id="name" name="name" />
하지만 implicit label의 경우 assistive technology가 제대로 인식하 지 못하는 경우도 존재하기 때문에 항상 explicit label을 사용하는 것이 좋다.
📎 시각적 순서와 DOM 순서가 일치하도록 작성하기
과거 경험이 떠오른다. 바텀 탭에 존재하는 버튼을 헤더 탭으로 CSS를 통해서 옮기라고 하는데, 해당 버튼을 헤더 탭 자식 요소로 넣을 수 없기 때문에 바텀 탭에 존재하는 상태에서 fixed, left, top 등의 css style을 이용하여 억지로 넣어줬다. 당시에 이렇게 하면 안될 것 같은데 그 이유를 논리적으로 설명하기가 어려웠으나 오늘 문서를 보면서 이유를 알게됐다.
스크린 리더가 다음과 같이 작성된 HTML 요소를 만나게되면 label에 적힌 내용부터 포커싱된다.
<label>나 라벨!</label> <input type="radio"/>
그렇다면 다음 HTML 요소를 만나게되면 어떤 값 부터 포커싱이 될까?
<div style={{ display: 'flex' }}> <label style={{ order: 2 }}>나 라벨!</label> <input style={{ order: 1 }} type="radio"/> </div>
예상과는 다르게 label 요소부터 포커싱이 된다. 스크린 확대기와 스크린 리더를 동시에 사용하는 저시력자의 경우, 시각적인 순서와 읽어주는 순서가 일치하지 않는 경우 혼란에 빠질 수 있다. 그렇기 때문에 애시당초 다음과 같이 작성해주는 것이 좋다.
<div style={{ display: 'flex' }}> <input /> <label>나 라벨!</label> </div>
https://css-tricks.com/html-inputs-and-labels-a-love-story/
📎 Tailwind의 동작
tailwind는 빌드할 때 애플리케이션을 스캔하면서 사용되는 class들만 남기고 나머지는 전부 purge한다. 그렇기 때문에 애플리케이션 사이즈가 아무리 커도 CSS 파일이 10kb를 넘지 못한다.
완벽하지 않은 dynamic class name을 사용하는 방법은 safelist를 사용하는 방법 외에는 존재하지 않는다. safelist는 purge에서 제외하기 때문이다. 완벽한 dynamic class name은 여전히 사용 가능하다.
// 완벽하지 않은 dynamic class name <div class="text-{{ error ? 'red' : 'green' }}-600"></div> // 완벽한 dynamic class name <div class="{{ error ? 'text-red-600' : 'text-green-600' }}"></div>
Dynamically build classnames in TailwindCss
📎 placeholder 스타일 정하기
적용하는 요소의 className에 직접 기입하거나, 커스텀 클레스를 만들거나, base style을 바꾸어 전역적으로 적용되게 가능하다.
3 Simple Methods to Style Placeholder Text in Tailwind CSS
📎 reset.css 로드하지 않기
tailwind는 기본적으로 normalize.css를 기반으로 한 predefined css가 존재하기 때문에 reset.css를 로드하지 않아도 된다.
더불어서 normalize.css랑 reset.css랑 다른데 둘 다 로드해도 문제가 없지 않나?로 생각할 수 있지만 굳이 그럴 필요 없다.
📎 @tailwind, @layer, @apply, base, components, utilities 정의
@tailwind 명령어는 기본적으로 base, components, utilities 세개의 스타일 레이어를 프로젝트에 삽입하기 위해 사용된다.
base의 경우 tailwind가 정의한 normalized.css와 유사하고, components의 경우
@layer를 통해서 커스텀하게 정의한 스타일이 base, components, utilities 중에 어디에 속하는지 정의한다. @layer를 사용하지 않고 클래스를 정의하는 경우 purge에서 제외된다.
@apply는 반복적으로 사용되는 유틸리티 패턴을 커스텀 클래스로 추출한다.
components는 card, btn, badge와 같이 복잡한 클래스인 경우 @layer components를 통해 지원할 수 있다.
utilities는 tailwind가 특정 CSS를 유틸리티 클래스로 지원하지 않는 경우 @layer utilities를 통해 지원할 수 있다.
컴포넌트를 나누는 이유는 두가지다. 첫번째는 재사용 목적이고, 두번째는 복잡성을 덜어내기 위한 목적이다.
재사용을 위해서는 (1) 일반화하기와 (2) 조건문 덜어내기가 필요하고, 복잡성을 덜어내기 위해서는 (1) 책임을 분리하고 (2) 비지니스 로직을 분리해야한다.
📎 일반화하기
기존에 만들어놓은 IconButtonGroup이라는 컴포넌트를 Tab 컴포넌트 구현시에 사용할 일이 있었다. IconButtonGroup이 Tab 컴포넌트에서 올바르게 표현되도록 하기 위해 IconButtonGroup을 수정할 일이 있었다.
컴포넌트는 보통 재사용 가능하다. 재사용 가능한 컴포넌트이려면 일반적(보편적, 포괄적)인 특징이 있어야한다.
컴포넌트를 재사용하는 과정에서 기존 컴포넌트를 수정할 일이 빈번한데, 이때 일반적인 특성을 유지하고 있는지 고민하자📎 조건문 덜어내기
컴포넌트에 조건문이 추가된다는 것은 좋은 신호가 아니다. 이는
상위 컴포넌트와 하위 컴포넌트가 강하게 결합
하는 신호일 수 있고, 이는상위 컴포넌트가 하위 컴포넌트에 강하게 의존
함을 의미한다. 이로인해 컴포넌트 자체의 변경이 어려워지고 하위 컴포넌트의 조그만 변경이 상위 컴포넌트의 버그로 이어질 수 있다.📎 비지니스 로직의 분리
View와 Business Logic은 변경의 속도가 다르다. View가 훨씬 빈번하다. 빈번한 변경 속에 Business Logic을 두는 것은 위험하고 찾기도 어렵다. 적절하게 분리하는 방법은 View의 상태로 관리하지 않고 Business Logic을 API 요청과 응답처럼 취급하는 것이다. Business Logic을 분리하기 위해 함수를 종종 이용하는데 이는 부족하다. Class를 이용하자
라고 언급하고 있으나 예시가 잘 이해가 안된다. Business Logic에 대해서도 현재 내가 맡고 있는 프로젝트에서 어떤 부분이 Business Logic인지도 잘 안떠오른다. 다른 글을 조금 더 읽어봐야 할듯.https://medium.com/@shinbaek89/프론트엔드-아키텍처-컴포넌트를-분리하는-기준과-방법-e7cf16bb157a
https://medium.com/@shinbaek89/ 프론트엔드-아키텍처-business-logic의-분리-adc10ae881ab
📎 전역 객체에 프로퍼티 추가하기
- window에 새로 추가되는 프로퍼티가 프로젝트 전반에 걸쳐서 사용되는지 아닌지가 중요한 듯.
- Window라는 식별자는 lib.d.ts에 존재하는 선언이다.
- declare global 내부에 선언된 타입들은 global scrop에 등록된다. 그렇기 때문에 .d.ts 확장자가 필요 없다.
- .d.ts 내부에 선언된 타입들은 global scope에 등록된다. 그렇기 때문에 declare global이 필요 없다.
📎 Tab 컴포넌트 목적
서로 관련이 있고, 동일한 계층에 존재하는 컨텐츠의 navigation을 돕는 요소
📎 Tab 컴포넌트 설계하기
선택한 탭에 대해서 패널들 중에 탭에 대응하는 패널만 보여지는데, 그렇다면 패널들에 대해 ul과 li를 써도 되는가?에 대한 고민이 있었는데, 패널들은 선택한 패널 하나만 보여지기 때문에 굳이 쓰진 않아도 될것 같다.
How to create an accessible Tabs component in React?의 문서에서 hidden을 쓰는데, 옳바른 쓰임세가 아닌 것 같다.
Creating an accessible tab component with React
Mastering React Design Patterns: Creating a Tabs Component
📎 section 태그와 article 태그
section
태그의 특징은 다음과 같다.- 각각의 section은 heading 태그가 존재한다.
- 주제별 컨텐츠를 그룹핑한다. 대표적인 예로 챕터가 있다.
- 페이지 내에 section이 하나인 경우, section을 사용하지 않는다.
article
태그의 특징은 다음과 같다.- 뉴스 기사, 블로그 게시글 등 "독립적으로 공유되는" 콘텐츠를 표현한다.
section 태그와 article 특징은 전혀 관련이 없다. 다른 주제를 가진 article들을 그루핑할 때 사용할 수도 있고, 하나의 article이 서로 다른 section 태그를 가질 수 있다. 혹은 동시에 사용되지 않을 수도 있다.
<article> <section id="introduction"> </section> <section id="content"> </section> <section id="summary"> </section> </article> // or... <section id="main"> <article> <!-- first blog post --> </article> <article> <!-- second blog post --> </article> <article> <!-- third blog post --> </article> </section>
How to correctly use "section" tag in HTML5?
📎 Compound 컴포넌트 패턴 소감
Component 컴포넌트 패턴 특징은 다음과 같다.
- 부모의 상태를 공유하기 위해 Context API가 사용된다.
- Compound 컴포넌트 패턴은 JSX가 컴포넌트 바깥으로 드러나는 특징이 있다.
좋았던 점은 Compound 컴포넌트를 구성하는 컴포넌트들이 자신에게 필요한 prop을 가져가기 때문에 알맞은 관심사 및 책임 분리가 가능했다. 더불어서 폭넓은 재사용성을 제공할 수 있었다. (아이콘 버튼 그룹 컴포넌트를 헤더에서 사용한다던지)
안좋았던 점은 다음과 같다.
- Compound 컴포넌트 내부에 들어가는 컴포넌트 명을 전부 알아야한다.
- JSX가 길어진다.
- ContextAPI가 딸려온다.
종합적으로보면, IconButtonGroup 컴포넌트처럼 UI가 간단한 경우에는 불리한 것 같다. 페이지 전반에 걸쳐서 복잡한 UI 규칙을 갖는 Header 컴포넌트 혹은 Tab 컴포넌트와 같은 경우에 유리한 것 같다.
📎 Linkable Tab 컴포넌트 설계하기
How to Create Linkable NextJS Tabs using HeadlessUI and TailwindCSS
📎 wai-aria에 관한 이해
WAI ARIA는 Web Accessiblity Initiate Accessible Rich Internect Application의 약자로, role과 attribute로 구성되어 스크린 리더를 도와 웹 접근성을 향상시킨다. 참고로 ARIA에 대해서는
"No ARIA is better than bad ARIA."
란 말이 있다accessible name이란 모든 UI 요소가 가져야하는 이름인데, 이 이름은 OS와 같은 플랫폼이 갖는 accessibility API를 통해서 Assistive Technologies에 전달된다. 크롬 개발자 도구를 통해서도 accessible name을 확인할 수 있다.
![]
accessible name은 보여지는 경우도 있고 보여지지 않는 경우도 있다. 보여지는 경우는 다시 accessible name이 요소 내부에 존재하는 경우 와 요소 외부에 존재하는 경우로 나뉜다. 요소 내부에 존재하는 경우는 <button/>, <a/>, <td/> 등의 태그들이 갖는 내부 텍스트들이 그 예이며, 요소 외부에 존재하는 경우는 <textarea/>, <fieldset/>, <table/> 등의 태그들이 각각 <label/>, <legend/>, <caption/> 태그들과 연결되는 경우이다. 반대로 보여지지 않는 경우는 <img/> 태그가 alt 태그를 갖는 경우다.
accessble name이 존재하지 않는 경우 aria-label과 aria-labelledby를 이용해서 만들어 줄수 있다. aria-labelledby는 id를 이용하여 다른 DOM 요소를 참조하여 accessible name을 결정할 때 사용되고, 참조할 DOM 요소가 없는 경우 aria-label을 이용한다.
aria-describedby도 aria-labelledby와 동일하게 동작하는데 labelledby는 describedby에 비해서 훨씬 짧은 정보를 제공한다는 차이점이 존재한다. aria-labelledby는 복잡한 규칙이 존재하니 사용에 유의하자.
ARIA: presentation role in Mozilla
ARIA Authoring Practices Guide (APG)
📎 hidden attribute의 의미
요소를 숨긴다. 이를 통해 react에서 요소를 숨기는 방법을 하나 더 알게됐다.
- conditional rendering
- display: none
- visibility: hidden
- hidden
display: none과 hidden은 거의 동일하다. (react에서의 동작도 동일하다) 단, 사용해야 하는 때는 다음과 같이 구분할 수 있다. hidden은 숨기려는 요소가 현재 페이지 컨텐츠의 일부가 아닐 때 사용(semantic한 관점)하고, display:none은 숨기려는 요소가 현재 페이지 컨텐츠의 일부일 때 사용한다.
예를들면 폼을 작성하는 와중에 유저가 잘못 기입했을 때 보여져야하는 에러 메세지나 혹은 폼을 작성하고나서 보여져야하는 성공 메세지는 hidden이 적합하다. 에러 메세지든 성공 메세지든 실제 조건이 충족되지 않는다면 보여지지 않기 때문에 페이지 컨텐츠의 일부로 보기 어렵기 때문이다
반면에 뷰포트 등의 제약으로 특정 문단을 숨겨놨다가 토글 버튼을 눌러서 보여주거나 혹은 유저가 어떤 요소를 Hover하거나 클릭했을 때 특정 요소를 보여주어야 하는 경우에는 display: none이 적합하다.
hidden이나 display: none 모두 스크린 리더가 읽어주지 않지만 간혹가다 읽어주는 경우도 존재한다고 한다.
tab panel을 설계한다면 display: none을하고 여기에 aria-hidden=true를 이용하여 스크린 리더가 읽어주지 않는 것을 보장해주자.
What is the difference between the hidden attribute (HTML5) and the display:none rule (CSS)?
Using the hidden attribute vs conditionally rendering a component
📎 Context API 사용하기
📎 Modal과 Dialog의 차이
Modal과 Dialog 모두 페이지 위에 쌓인다는 점에서 동일하지만, Modal은 등장하면 페이지의 상호작용이 불가하지만 Dialog는 등장해도 페이지의 상호작용이 가능하다는 차이가 존재한다.
Modals Will Never Be The Same - HTML dialog Element
📎 Snackbar와 Toast의 차이
Snackbar와 Toast 모두 화면 아래에 등장하며 피드백을 준다는 점에서 동일하지만, Snackbar는 유저가 상호작용 해야지만 사라지고, Toast는 유저가 상호작용 하지 않아도 사라진다.
Toast vs Snackbar: Choosing the Right Feedback Tool for Your Android App
📎 Overlay
씌우다, 깔다, 바르다라는 사전적 의미를 가짐. 현재 페이지 위에 쌓이는 레이어로 추가적인 정보와 선택지를 제공. 모달처럼 페이지의 사용을 중단하거나, 툴팁이나 팝오버처럼 보완하는 역할을 하기도 함
📎 Widget, Panel, Card, Tile 구분
집단마다 정의가 다르기 때문에 대략적으로 알아두면 좋다고한다.
Panel은 폼, 정보의 집합, 데이터 테이블 등 유사한 정보를 모아놓는 container다. 여러 개의 Widget이나 Card를 포함한다.
Widget은 작은 시각적 객체인데 사이트에 배치되는 검색 박스, 로그인 박스 등이 해당된다.
Card는 관련한 여러개의 정보를 하나에 모으며 페이지에 여러번 등장하고 드래그나 스와이프 등의 상호작용이 가능하다. Card 자체가 상호작용 가능한 요소를 포함하기도 한다.
Tile은 하나의 정보만을 가진 container로 아이콘 혹은 연관된 정보(온도와 습도)등을 포함하며 광범위한 정보를 다루지 않음.
📎 Decorator 사용시 tsx 사용하지 못해서 발생하는 에러