Cmarket의 접근성을 개선 해보자💌

돌리의 하루·2023년 3월 3일
0

얼마 전에 했던 Cmarket - hooks를 웹표준에 맞게 개선하고 시멘틱 요소를 살려보려고 한다! 🐹✨

( 참고로 Cmarket hooks는 대부분 div, span 요소를 사용했다 )

그림을 참고해서 보니 좀 더 알기쉬웠다.

//Header.js
function Header() {

  const state = useSelector(state => state.itemReducer);

  return (
    // div -> header로 시맨틱 요소 사용
    <header>
      {/* 인라인 요소인 span이 블록 요소인 h1 요소를 담으므로 div로 변경 */}
      {/* 요소의 배치가 목적이라면 div 사용도 적합 */}
      <div id="title-container">
        <img id="logo" src="../logo.png" alt="logo" />
        {/* span -> h1으로 시맨틱 요소 사용 */}
        <h1>CMarket</h1>
      </div>
      {/* div -> nav로 시맨틱 요소 사용 */}
      <nav>
        <Link to="/">상품리스트</Link>
        <Link to="/shoppingcart">
          장바구니<span id="nav-item-counter">{state.cartItems.length}</span>
        </Link>
      </nav>
    </header>
  );
}
  
export default Header;
  • hgroup 요소 ( h1, h2 ...) 를 사이즈 변경을 위해 쓰면 안된다.
    보통 재사용할 수 있고 독립적인 컨텐츠인 article을 식별하기 위해 hgroup을 붙여주기도 한다.

  • nav는 눌렀을 때 다른 탭이나 홈페이지로 이동하는 부분에 만들어준다.
//Item.js
export default function Item({ item, handleClick }) {

  return (
    // div -> li 요소로 변경
    <li key={item.id} className="item">
      {/* 상품 이름과 중복되므로 대체 텍스트로 빈 문자열 작성 */}
      <img className="item-img" src={item.img} alt="" ></img>
      {/* span -> h3으로 시맨틱 요소 사용 */}
      <h3 className="item-name" data-testid={item.name}>{item.name}</h3>
       {/* 딱 적절한 시맨틱 요소가 없고, className으로 판단할 수 있으므로 span요소 유지 */}
      <span className="item-price">{item.price}</span>
      <button className="item-button" onClick={(e) => handleClick(e, item.id)}>장바구니 담기</button>
    </li>
  )
}
  • item 하나의 컴포넌트를 나타내는 item.js는 <li>로 감싸준다.
  • <li><ul>이나 <ol>로 감싸줘야 한다! 💜
  • <img>에 alt로 대체 텍스트를 붙여줘야하는데, <h3>부분에 보면 {item.name}으로 값을 준것을 볼 수 있다.
    여기서 img의 대체 텍스트에도 {item.name}을 그대로 붙여준다면, 중복해서 읽기 때문에 alt값에 빈 값인 ""을 주었다.
//shoppingCart.js
export default function ShoppingCart() {

  const state = useSelector(state => state.itemReducer);
  const { cartItems, items } = state
  const dispatch = useDispatch();
  const [checkedItems, setCheckedItems] = useState(cartItems.map((el) => el.itemId))

  const handleCheckChange = (checked, id) => {
    if (checked) {
      setCheckedItems([...checkedItems, id]);
    }
    else {
      setCheckedItems(checkedItems.filter((el) => el !== id));
    }
  };

  const handleAllCheck = (checked) => {
    if (checked) {
      setCheckedItems(cartItems.map((el) => el.itemId))
    }
    else {
      setCheckedItems([]);
    }
  };

  const handleQuantityChange = (quantity, itemId) => {
    dispatch(setQuantity(itemId, quantity));
  }

  const handleDelete = (itemId) => {
    setCheckedItems(checkedItems.filter((el) => el !== itemId))
    dispatch(removeFromCart(itemId))
  }

  const getTotal = () => {
    let cartIdArr = cartItems.map((el) => el.itemId)
    let total = {
      price: 0,
      quantity: 0,
    }
    for (let i = 0; i < cartIdArr.length; i++) {
      if (checkedItems.indexOf(cartIdArr[i]) > -1) {
        let quantity = cartItems[i].quantity
        let price = items.filter((el) => el.id === cartItems[i].itemId)[0].price

        total.price = total.price + quantity * price
        total.quantity = total.quantity + quantity
      }
    }
    return total
  }

  const renderItems = items.filter((el) => cartItems.map((el) => el.itemId).indexOf(el.id) > -1)
  const total = getTotal()

  return (
    // div -> main으로 시맨틱 요소 사용
    <main>
      {/* div -> section으로 시맨틱 요소 사용 */}
      <section id="item-list-body">
        {/* div -> h2로 시맨틱 요소 사용 */}
        <h2>장바구니</h2>
        {/* 인라인 요소가 인라인 요소를 담고 있는 것은 괜찮음 */}
        <span id="shopping-cart-select-all">
          <input
            // id로 input 요소와 연결
            id="select-all-checkbox"
            // title로 레이블 작성
            title="장바구니 아이템 모두 선택하기"
            type="checkbox"
            checked={
              checkedItems.length === cartItems.length ? true : false
            }
            onChange={(e) => handleAllCheck(e.target.checked)} >
          </input>
          {/* for 속성으로 input 요소와 연결 */}
          <label for= "select-all-checkbox" >전체선택</label>
        </span>
        <div id="shopping-cart-container">
          {!cartItems.length ? (
            <div id="item-list-text">
              장바구니에 아이템이 없습니다.
            </div>
          ) : (
              <ul id="cart-item-list">
                {renderItems.map((item, idx) => {
                  const quantity = cartItems.filter(el => el.itemId === item.id)[0].quantity
                  return <CartItem
                    key={idx}
                    handleCheckChange={handleCheckChange}
                    handleQuantityChange={handleQuantityChange}
                    handleDelete={handleDelete}
                    item={item}
                    checkedItems={checkedItems}
                    quantity={quantity}
                  />
                })}
              </ul>
            )}
          <OrderSummary total={total.price} totalQty={total.quantity} />
        </div>
      </section >
    </main>
  )
}
  • input에는 식별할 수 있는 레이블을 붙여주는 것이 좋은데, label for을 사용하거나 title을 사용해서 붙여준다.
  • input값에 id를 붙여주고, label for="input의 id"를 적어주면 label의 위치가 어디있던 label값을 누를 때 input 값으로 초점이 맞춰지는걸 볼 수 있다.
  • main 안에 section을 뒀다. section은 전체적인 내용과 관련이 있는 콘텐츠들의 집합으로, 딱히 적합한 의미의 요소가 없을 때 사용한다.
//OrderSummery.js
export default function OrderSummary({ totalQty, total }) {
  return (
    // div -> aside로 시맨틱 요소 사용
    // aside 사용한 이유 : 콘텐츠와 관련이 있으면서 구분 가능한 내용이기 때문
    <aside id="order-summary-container">
      <h4>주문 합계</h4>
      <div id="order-summary">
        총 아이템 개수 : <span className="order-summary-text">{totalQty}</span>
        {/* hr도 시맨틱 요소이므로 CSS로 대체 */}
        <div id="order-summary-total">
          합계 : <span className="order-summary-text">{total}</span>
        </div>
      </div>
    </aside >
  )
}
  • aside는 본문의 주요 부분 이외의 컨텐츠를 나타낼 때 사용한다. ex)사이드바 or 광고창
  • 본문에서는 주문 합계를 나타내는 합계금액의 구역을 aside로 줬다.
  • 또한, 합계 : {total}원 부분에 10,000원 이상이면 스크린 리더기가 만원단위가 아니라 숫자로 읽게 되는데,
    이 부분에 10,000원 처럼 콤마를 중간에 찍어주는 메서드.toLocalString()을 사용하게 되면 스크린 리더기가 잘 작동하는것을 볼 수 있다.
profile
진화중인 돌리입니다 :>

0개의 댓글