[React] - 접근성을 위한 Dialog, Hooks의 배경

Lee Jeong Min·2021년 12월 21일
0
post-thumbnail

SKHeading 컴포넌트

propTypes

참고 사이트: https://ko.reactjs.org/docs/typechecking-with-proptypes.html#gatsby-focus-wrapper

// React 컴포넌트 필히 가지는 속성
// defaultProps
// displayName
// propTypes

SkHeading.defaultProps = {
  // as: 'h2',
  className: '',
  isVisible: true,
};

// 컴포넌트 props를 type check 하는 모듈
export const PropTypes = {
  string(props, propName, componentName) {
    // 조건 확인
    let value = props[propName];
    if (!isString(value)) {
      throw new Error(
        `${componentName} 컴포넌트에 잘못된 ${propName}이 전달되었습니다. 기대되는 prop 타입은 string 이어야 하나, 전달된 prop 타입은 ${typeIs(
          value
        )} 입니다.`
      );
    }
  },
};

SkHeading.propTypes = {
  as: PropTypes.string,
  className: PropTypes.string,
};

// 디버깅 관점에서 중요하고, 없다면 function의 이름으로 사용
SkHeading.displayName = 'SKH';

웹 표준과 웹 접근성을 위한 Dialog

키보드 트랩

참고사이트: https://www.w3.org/TR/wai-aria-practices-1.1/examples/dialog-modal/dialog.html

Focusable Elements

  • HTMLAnchorElement - has href attribute
  • HTMLAreaElement - has href attribute
  • HTMLButtonElement
  • HTMLInputElement
  • HTMLSelectElement
  • HTMLTextAreaElement
  • HTMLIframeElement
  • HTMLVideoElement - has controls attribute
  • HTMLAudioElement - has controls attribute
  • HTMLSummaryElement
  • HTMLDetailsElement
  • has tabindex attribute
  • has contenteditable attribute

isFocusbale.js

const focusableElementSelector =
  'a[href], area[href], button, input, select, textarea, iframe, summary, details, video[controls], audio[controls], [contenteditable=""], [contenteditable="true"], [tabindex]:not([tabindex^="-"])';

const isFocusable = (elementNode) => {
  const current = document.activeElement;
  if (current === elementNode) return true;

  const protectEvent = (e) => e.stopImmediatePropagation();
  elementNode.addEventListener('focus', protectEvent, true);
  elementNode.addEventListener('blur', protectEvent, true);
  elementNode.focus({ preventScroll: true });

  const result = document.activeElement === elementNode;
  elementNode.blur();

  if (current) current.focus({ preventScroll: true });
  elementNode.removeEventListener('focus', protectEvent, true);
  elementNode.removeEventListener('blur', protectEvent, true);

  return result;
};

const getFocusableChildren = (node) => {
  const children = Array.from(node.querySelectorAll(focusableElementSelector)); // NodeList => Array
  return children.filter((child) => isFocusable(child));
};

export { isFocusable, getFocusableChildren };

focusbale 요소를 확인하기 위한 유틸리티 함수

    const handleEvent = (e) => {
      const { key, shiftKey, target } = e;
      
      if (
        Object.is(target, firstFocusableElement) &&
        shiftKey &&
        key === 'Tab'
      ) {
        e.preventDefault();
        lastFocusableElement.focus();
      }

      if (
        Object.is(target, lastFocusableElement) &&
        !shiftKey &&
        key === 'Tab'
      ) {
        e.preventDefault();
        firstFocusableElement.focus();
      }

      if (key === 'Escape') {
        this.props.onClose();
      }
    };

시프트 탭 키와 탭키의 키보드 트랩을 위한 코드

오프너 버튼에 초점 이동

다이얼로그 창을 닫았을 때 키보드 초점 이동

  ...

  opennerButton = null;

  ...

  componentDidMount() {
    // 모달 다이얼로그가 화면에 표시되는 시점에 라이프 사이클 메서드 실행
    // 그렇다면? 이 실행 시점에 문서의 현재 활성화 된 요소 노드(document.activeElement)는?
    this.opennerButton = document.activeElement;

    this.unbindKeyEvent = this.bindKeyEvent();
  }

  componentWillUnmount() {
    this.unbindKeyEvent?.();
    this.opennerButton.focus();
  }

이런 방법으로 해주어도 되고 함수를 만들어서 전달받은 prop 콜백을 안에 넣어서 사용하는 방법또한 있음(close() 메서드를 만들어서)

컴파운드 컴포넌트 (이름이 있는 콘텐츠 슬롯과 유사)

바깥쪽에서 컴포넌트를 가지고 재조립을 하여 사용

  // 컴파운드 컴포넌트 (클래스 멤버)
  static Dim = function DialogDim({ onClose }) {
    return <div role="presentation" className="dim" onClick={onClose} />;
  };

화살표 함수를 사용하게 되면 react dev tool의 component 부분에서 이름이 안나오는 anonymous로 표기 되기 때문에 함수에 이름을 주기 위해 위와 같이 설정

SkSpinner 컴포넌트

접근성을 위해 aria-live의 assertive 가 필요

참고 사이트: https://aoa.gitbook.io/skymimo/aoa-2018/2018-aria/loading

React에서는 vdom이기 때문에 이를 위해선 실제 dom이 필요하고 react portal을 이용해 구현!

React Hooks

Hooks이 나오게 된 배경

  • Wrapper Hell

  • Huge Components

컴포넌트에 보다 많은 로직을 작성해 래퍼 지옥 문제를 해결하려고 하면 컴포넌트가 커지고 리팩토링 하기 매우 어려워짐

  • Confusing classes

React 팀의 새로운 제안

React Hooks을 사용하면 클래스를 사용하지 않아도 함수 컴포넌트를 중심으로 앱 개발이 가능하다. 또한 Hooks는 함수 컴포넌트에서 사용할 수 없었던 state, refs, context, side effects 처리 등과 같은 React의 콘셉을 보다 직관적으로 사용할 수 있는 API를 제공함.

발표 이후 반응

React를 이전보다 쉽게 사용할 수 있도록 하여 긍정적인 반응

코드량의 차이

Hooks, 로직 재사용

Hooks은 일종의 함수이고 함수는 코드를 재사용하기 위한 완벽한 메커니즘이다. 래퍼 지옥 등 고통스러웠던 이전의 React 문제를 Hooks가 해결함

캡슐화

Hook은 완전하게 캡슐화 처리되어 현재 실행 중인 컴포넌트에서 Hook을 호출할 때에도 격리된 로컬 상태를 유지한다. 결과적으로 Hook은 React의 단방향 데이터 흐름을 깨트리지 않는다.

클린 트리

Hook 간에 데이터를 전달하는 기능은 Hooks를 애니메이션, 구독, 폼 관리 등을 처리하는데 매우 적합하며 컴포넌트에 연결된 메모리 셀의 단순한 리스트와 유사하다.

상태 유지 위치

결론은 React가 클래스 컴포넌트 상태를 유지하는 방법과 똑같은 위치에 유지한다.

참고사이트: https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

보다 작은 번들 크기

클래스를 사용할 때 보다 번들된 파일의 크기를 줄임

실습

const anyState = useState([]);

console.log(anyState)

useState를 찍어보면 다음과 같이 나온다.

각각 state, setState함수 (클래스와 비교시)
따라서 구조분해 할당으로 자신이 원하는 관심사의 상태와 상태를 관리하는 함수를 가져와 사용할 수 있다.(관심사 분리 가능)

  // 어떤 상태를 관리할 것인가?
  const [card, setCards] = useState([]); // [state, setState반환]

useEffect()

이 세가지 라이프 사이클

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

과 같은 목적으로 제공되며 하나의 API로 통합된 것이다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글