어느 날 동료에게서 체크박스에 관한 질문을 받았다. 체크박스를 여러 개 선택한 후 선택된 값들을 어떻게 저장해야 할지 감이 오지 않는다는 것이었다. 화면상에서 체크했다 풀었다 하는 것은 쉽지만, 이를 서버에 보내려면 체크된 값들을 관리해야 하는데 이 부분에서 어려움을 겪고 있다고 했다. 마침 내가 개발한 페이지에도 비슷한 기능이 있었고 이미 구현을 마쳐놓은 상태여서 족집게 강의로 비법을 전수해주기로 했다.
아뿔싸! 그런데 동료의 코드를 살펴보니 체크박스고 버튼이고 할 것 없이 전부 div로 되어 있었다. emotion이나 styled-components 같은 CSS-in-JS 라이브러리를 쓰다 보면 JSX에 HTML 태그 대신 사용자가 정의한 이름을 사용하기 때문에 semantic markup에 신경 쓰기가 어려워지는 것 같다. 겉보기에 별다른 문제는 없겠지만, 웹 접근성 측면에서도 좋지 않고 HTML element마다 내장되어 있는 기본 속성들도 사용할 수 없게 된다. 모든 element에 div를 사용하는 것은 모든 API에 POST 메서드를 사용하는 것과 다를 바가 없다. 그래서 마크업부터 상태 관리, 스타일링까지 동료에게 상세하게 설명해주었다.
예시 코드 전체 보기: https://codesandbox.io/s/checkbox-list-8wzgkm
<input type="checkbox">
를 사용한다. 체크 여부를 boolean으로 나타내는 checked
속성과 input의 값이 바뀔 때 실행되는 onChange
함수를 사용할 수 있다.input
은 label
과 함께 사용한다. label 안의 어느 부분을 누르더라도 체크가 되므로 작은 체크박스를 클릭하는 것보다 편하다. 아래 코드처럼 label 안에 input을 넣어도 되고, label의 htmlFor
, input의 id
속성을 활용하여 둘을 짝지어주는 방법도 있다.const ListItem = (props) => {
return (
<li>
<label>
<input type="checkbox" checked onChange={() => {}} />
<span>{props.log.date} 운동 기록</span>
</label>
</li>
);
}
ul
이나 ol
로 감싸고, map으로 렌더링되는 컴포넌트가 li
로 시작하는 것이 좋다.button
태그를 사용해야 onClick 이벤트를 핸들링하는 것이 자연스럽고, type
이나 disabled
같은 기본 속성도 활용할 수 있다.const List = () => {
return (
<div>
<h2>리포트 목록</h2>
<ul>
{DUMMY_EXERCISE_LOG.map((log) => (
<ListItem key={log.date} log={log} />
))}
</ul>
<button type="button">리포트 보기</button>
</div>
);
}
useState
로 관리한다. POST 요청을 보낼 때 어떤 데이터를 담아 보내야 하는지에 따라 배열에 무엇을 담을지가 결정된다. 각 항목의 id가 될 수도 있고, value가 될 수도 있다. 여기서는 날짜별로 리포트를 조회하는 것이므로 날짜를 담았다.const List = () => {
const [selectedDates, setSelectedDates] = useState([]);
return (
<div>
<h2>리포트 목록</h2>
<ul>
{DUMMY_EXERCISE_LOG.map((log) => (
<ListItem
key={log.date}
log={log}
selectedDates={selectedDates}
setSelectedDates={setSelectedDates}
/>
))}
</ul>
<button type="button">리포트 보기</button>
</div>
);
}
includes
메서드를 사용하여 selectedDates 배열에 현재 항목의 날짜가 들어있으면 true, 없으면 false로 설정한다.filter
메서드를 사용하면 손쉽게 값을 추가하거나 삭제할 수 있다.sort
메서드를 사용해 내림차순으로 정렬했다. 이때 sort는 원본 배열을 바꿔버리므로 스프레드 연산자를 사용해 배열을 복제 후 정렬해야 한다.const ListItem = (props) => {
const isChecked = props.selectedDates.includes(props.log.date);
const onChangeCheck = () => {
if (!isChecked) {
props.setSelectedDates((prev) => [...prev, props.log.date]);
} else {
props.setSelectedDates((prev) =>
prev.filter((date) => date !== props.log.date)
);
}
props.setSelectedDates((prev) =>
[...prev].sort((a, b) => new Date(b) - new Date(a))
);
};
// JSX 생략
}
checked
속성과 className
, 아이콘 등을 적절히 활용하여 스타일링을 해준다.const ListItem = (props) => {
// 이전 코드 생략
return (
<li className={isChecked ? "checked" : ""}>
<label>
<input type="checkbox" checked={isChecked} onChange={onChangeCheck} />
{isChecked && <CheckIcon />}
<span>{props.log.date} 운동 기록</span>
</label>
</li>
);
}
appearance: none
을 설정하여 기본 스타일을 없애고, :checked
셀렉터를 사용해 새로운 스타일을 입히면 된다.input[type="checkbox"] {
appearance: none;
border: 1px solid gray;
}
input[type="checkbox"]:checked {
background: navy;
border: none;
}
이렇게 해서 다중선택 체크박스의 상태관리 방법과 커스텀 스타일 적용 방법까지 동료에게 차근차근 설명해주었다. 설명을 듣고 동료도 이 로직을 적용하여 체크박스 기능을 완성할 수 있었다. 오늘도 누군가에게 도움을 줄 수 있어 뿌듯한 하루였다.