다중선택 체크박스 상태관리

ChaeChae·2022년 11월 3일
0
post-thumbnail

어느 날 동료에게서 체크박스에 관한 질문을 받았다. 체크박스를 여러 개 선택한 후 선택된 값들을 어떻게 저장해야 할지 감이 오지 않는다는 것이었다. 화면상에서 체크했다 풀었다 하는 것은 쉽지만, 이를 서버에 보내려면 체크된 값들을 관리해야 하는데 이 부분에서 어려움을 겪고 있다고 했다. 마침 내가 개발한 페이지에도 비슷한 기능이 있었고 이미 구현을 마쳐놓은 상태여서 족집게 강의로 비법을 전수해주기로 했다.

아뿔싸! 그런데 동료의 코드를 살펴보니 체크박스고 버튼이고 할 것 없이 전부 div로 되어 있었다. emotion이나 styled-components 같은 CSS-in-JS 라이브러리를 쓰다 보면 JSX에 HTML 태그 대신 사용자가 정의한 이름을 사용하기 때문에 semantic markup에 신경 쓰기가 어려워지는 것 같다. 겉보기에 별다른 문제는 없겠지만, 웹 접근성 측면에서도 좋지 않고 HTML element마다 내장되어 있는 기본 속성들도 사용할 수 없게 된다. 모든 element에 div를 사용하는 것은 모든 API에 POST 메서드를 사용하는 것과 다를 바가 없다. 그래서 마크업부터 상태 관리, 스타일링까지 동료에게 상세하게 설명해주었다.

예시 코드 전체 보기: https://codesandbox.io/s/checkbox-list-8wzgkm

체크박스 리스트 마크업

ListItem.js

  • 체크박스에는 <input type="checkbox">를 사용한다. 체크 여부를 boolean으로 나타내는 checked 속성과 input의 값이 바뀔 때 실행되는 onChange 함수를 사용할 수 있다.
  • inputlabel과 함께 사용한다. 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>
  );
}

List.js

  • 배열 데이터를 map으로 순회하며 ListItem 컴포넌트를 렌더링한다. 이처럼 map으로 리스트를 렌더링할 때는 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>
  );
}

체크된 항목의 상태 관리

List.js

  • 다중 선택이 가능한 체크박스이므로 선택된 항목을 배열에 담아 useState로 관리한다. POST 요청을 보낼 때 어떤 데이터를 담아 보내야 하는지에 따라 배열에 무엇을 담을지가 결정된다. 각 항목의 id가 될 수도 있고, value가 될 수도 있다. 여기서는 날짜별로 리포트를 조회하는 것이므로 날짜를 담았다.
  • ListItem 컴포넌트에 props로 state와 setState 함수를 전달해준다.
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>
  );
}

ListItem.js

  • 체크 여부를 나타내는 isChecked 변수를 정의한다. includes 메서드를 사용하여 selectedDates 배열에 현재 항목의 날짜가 들어있으면 true, 없으면 false로 설정한다.
  • 체크 상태가 변할 때마다 실행될 event handler 함수를 정의한다. 현재 항목이 체크가 안된 상태이면 selectedDates 배열에 추가하고, 체크된 상태이면 배열에서 제거해준다. 스프레드 연산자와 filter 메서드를 사용하면 손쉽게 값을 추가하거나 삭제할 수 있다.
  • 날짜나 숫자 같은 경우 정렬이 필요할 수도 있다. 내가 구현한 컴포넌트에서는 최근 기록부터 내림차순으로 렌더링해야 했다. string으로 되어 있는 날짜를 Date 객체로 변환하면 대소 비교를 할 수 있고, 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 생략
}

체크박스 스타일 커스터마이징

ListItem.js

  • 체크박스는 기본적으로 브라우저의 default style이 적용되어 예쁘지 않다. 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>
  );
}

style.css

  • 체크박스 input에 appearance: none을 설정하여 기본 스타일을 없애고, :checked 셀렉터를 사용해 새로운 스타일을 입히면 된다.
input[type="checkbox"] {
  appearance: none;
  border: 1px solid gray;
}

input[type="checkbox"]:checked {
  background: navy;
  border: none;
}

이렇게 해서 다중선택 체크박스의 상태관리 방법과 커스텀 스타일 적용 방법까지 동료에게 차근차근 설명해주었다. 설명을 듣고 동료도 이 로직을 적용하여 체크박스 기능을 완성할 수 있었다. 오늘도 누군가에게 도움을 줄 수 있어 뿌듯한 하루였다.

profile
정리 장인 && FE 개발자

0개의 댓글