얼마 전에 했던 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()
을 사용하게 되면 스크린 리더기가 잘 작동하는것을 볼 수 있다.