
어느 날 동료에게서 체크박스에 관한 질문을 받았다. 체크박스를 여러 개 선택한 후 선택된 값들을 어떻게 저장해야 할지 감이 오지 않는다는 것이었다. 화면상에서 체크했다 풀었다 하는 것은 쉽지만, 이를 서버에 보내려면 체크된 값들을 관리해야 하는데 이 부분에서 어려움을 겪고 있다고 했다. 마침 내가 개발한 페이지에도 비슷한 기능이 있었고 이미 구현을 마쳐놓은 상태여서 족집게 강의로 비법을 전수해주기로 했다.
아뿔싸! 그런데 동료의 코드를 살펴보니 체크박스고 버튼이고 할 것 없이 전부 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;
}
이렇게 해서 다중선택 체크박스의 상태관리 방법과 커스텀 스타일 적용 방법까지 동료에게 차근차근 설명해주었다. 설명을 듣고 동료도 이 로직을 적용하여 체크박스 기능을 완성할 수 있었다. 오늘도 누군가에게 도움을 줄 수 있어 뿌듯한 하루였다.