체크박스 전체 선택/일부 선택 구현

문다현·2023년 12월 10일
0
post-thumbnail

체크박스를 전체 선택/해제하거나 일부 선택/해제하여 포스트를 삭제할 수 있는 기능을 구현하던 중에, 너무 좋은 내용들을 많이 배운 것 같아서 기록하고자 합니다

목적

서버와의 http 통신을 할 때 , 즉 게시물 다중 삭제 api를 쏠 때, request body안에 게시물의 postId를 number 형식으로 배열로 묶어 보내야 합니다

현재 상황 파악

나의 post 불러오기 api를 통해 내가 쓴 게시물들의 list를 map을 돌려서 보여주고 있습니다

밑 타입은 내 게시물의 정보list들입니다. post의 list를 순회하면서 각 post의 postId를 가져오면 되겠습니다.

interface PostingPreviewProps {
  content: string;
  postId: string;
  userId: string;
  nickname: string;
  title: string;
//그외 등등
}

아래와 같은 조건들을 충족해야 합니다

1) 모든 게시물에 대해 체크 여부를 관리한다
2) 체크가 되어있는 게시물을 삭제하고 싶은 게시물로 간주한다
3) 전체 선택을 누르면 모든 게시물들을 체크한다

초기화

개별 포스트에 대해서 해당postId가 무엇인지, 또 해당post를 삭제하고 싶은지 값을 객체로 만들고 상태로 생성하여 관리하겠습니다.
posts가 존재하는지 체크한 다음,

posts가 있는 경우, posts를 순회하면서 뽑아온 postId와, 삭제 여부를 관리하는 boolean값 isChecked를 속성값으로 갖게 되는 객체 배열로 초기화합니다.
posts 가 없는 경우는 []로 초기화를 합니다.

  const [postIdList, setPostIdList] = useState<{ postId: string; isChecked: boolean }[]>(
    posts
      ? posts.map((item) => ({ postId: item.postId, isChecked: false }))
     : []
  );

checkbox 생성

총 두가지 종류의 체크박스를 만들어야 합니다

1) 전체 선택을 할 수 있는 체크박스
2) 포스트 별로 선택을 할 수 있는 개별 체크박스

전체 선택

		  const allChecked = postIdList?.length > 0 && postIdList.every((item) => item.isChecked === true);

          <label className={`${buttonStyle} ${allChecked ? "bg-green-500" : "bg-white"} `}>
            <input
              type="checkbox"
              checked={allChecked}
              onChange={(e) => handleSelectAll(e.target.checked)}
            />
            전체 선택
          </label>

input태그는 기본적으로 checked속성(boolean값)을 지원합니다. rue일 경우 체크가 되고 false일 경우 체크가 해제됩니다.
label태그는 input태그와 함께 쓸 수 있는데, 설명을 제공하는 역할을 한다. label태그로 input태그를 감싸게 되면, input에 대한 동작이 label를 클릭해도 동작하므로, 접근성과 사용자 경험을 개선하는 데 도움을 줍니다.

checked에 allChecked라는 변수를 넣을 것입니다.
allChecked는 postIdList가 있고, postIdList의 모든 인덱스 isChecked 속성이 true일 때 true가 되는 변수입니다.

이 때!! 흔히 할 수 있는 실수는,
allChecked를 별도의 상태로 생성하고, postIdList를 useEffect에서 변화를 추적하여
postIdList의 모든 요소의 isChecked가 true일 경우,
allChecked 상태를 true로 업데이트하는 방식으로 코드를 작성하는 것입니다.

대략 이러한 형태를 띌 것입니다

const [allChecked, setAllChecked] = useState(false);

useEffect(() => {
  setAllChecked(postIdList.length > 0 && postIdList.every((item) => item.isChecked));
}, [postIdList]);

어느 방향으로 코드를 짜던, 정상적으로 동작을 하긴 합니다. 상태보다는 변수로 관리하는 방식이 더 좋다생각합니다.
useEffect는 side effect를 처리하기 위한 도구로, 네트워크, 또는 브라우저 DOM과 같은 외부 시스템과 동기화 를 처리하는 데에 적합하고 그 외의 상태 업데이트는 사용에 신중해야합니다. 그 외의 상태 업데이트에 useEffect를 사용하는 데에는 신중해야 하는데, 그 이유는 다음과 같습니다.

1. 비동기 로직을 다룰 때 예상치 못한 결과 발생
비동기 로직을 useEffect에서 다룰 경우, 의존성 배열에 따라 비동기 함수가 다시 실행되면서 예기치 못한 결과나 오류가 발생할 수 있습니다.
2. 불필요한 성능 저하
useEffect는 의존성 배열에 있는 값이 변경될 때마다 다시 실행되므로, 불필요한 useEffect 사용은 성능에 악영향을 줄 수 있습니다.
3. React의 선언적 패러다임에 위배
특정 작업이 실행되도록 useEffect에서 강제하는 것은 React의 선언적 패러다임을 해칠 수 있습니다.

https://ko.react.dev/learn/you-might-not-need-an-effect#

리액트 공식문서에도 해당 내용에 대해 자세히 써있으므로, 보는 것을 강력 추천합니다.

전체 선택 체크박스 핸들러

  const handleSelectAll = (isChecked: boolean) => {
    setPostIdList(
      postIdList.map((item) => ({ ...item, isChecked: isChecked }))
    );
  };}

e.target.checked를 통해 전체선택 체크박스의 체크 여부를 boolean값으로 가져와서, postIdList의 모든 요소의 isChecked값을 e.target.checked값으로 일괄적으로 변경합니다.

다시 말해, 스프레드 연산자를 사용하여 현재의 item 객체를 복사한 후,
item의 모든 기존 속성은 그대로 두고, isChecked 속성만 e.target.checked 매개변수의 값으로 변경한 값으로 상태 업데이트를 합니다

개별 선택

   <input
      type="checkbox"
      checked={ postIdList.find((item) => item.postId === postId)?.isChecked }
      onClick={(e) => e.stopPropagation()}
      onChange={() => handleCheckboxChange(postId)}
    />

개별선택은 해당 포스트의 체크 여부만을 변경해야합니다. postIdList를 순회하면서 해당 postId와 일치하는 객체를 찾은 후, 그 인덱스의 isChecked 값 checked속성에 넣습니다.
post를 클릭하면 해당 포스트의 상세페이지로 라우팅이 되게 로직을 짰습니다. 체크박스를 사용 중일 때는 라우팅을 원하지 않으므로e.stopPropagation() 을 설정함으로써 상위 DOM 요소로의 이벤트 버블링을 막았습니다.

개별 선택 체크박스 핸들러

  // 개별 선택 핸들러
  const handleCheckboxChange = (postId: string) => {
    setPostIdList(
      postIdList.map((item) =>
        item.postId === postId ? { ...item, isChecked: !item.isChecked } : item
      )
    );
  };

postId가 일치하면 postIdList의 요소를 찾아서, isChecked값을 현재 boolean값과 반대되는 값으로 업데이트를 합니다.

onClick vs onChange

사실 그전에 핸들러를 onClick에 전달했었는데 예상대로 작동하지 않았었습니다. 체크박스를 클릭함으로써 상태변화가 일어나는 것인데 왜 onChange에 전달하지않으면 안되는 것일까요?

일단 이유 첫번째는 전체 선택을 통해 체크 상태를 업데이트하는 경우에는, 개별 체크박스를 클릭하는 것이 아니므로 변화를 감지하지 못합니다.

이유 두번째는 그렇게 한다면, 이벤트 핸들러에서 전달해준 e.target.checked값과 별개의 checked 상태를 생성해서, 두 값을 동기화시켜아하는 데 그 과정이 복잡해지거나 부정확할 수 있습니다. 또 불필요하기도 합니다.
onChange가 폼 요소(예: input, textarea, select)의 값이 변경될 때 상태 변화를 직접적으로 반영하기 때문에 더 적절하다.

결론

사실은 그 이전에도 onHandle이라던지, input checkbox과 관련한 코드를 많이 짜봤었다. 하지만 왜 그렇게 구현을 해야 하고, 어느 방식이 더 효율적이고, 어떠한 동작이 어떠한 동작을 부르는지에 대한 내부적인 요소에 대해서는 무지했던 것 같다. 새로운 기술을 도입하고 학습하는 것도 물론 좋지만 기본기가 탄탄해야 그 기술들을 진정히 받아들일 수 있음을 인지하고 앞으로도 계속 성장해나가자

profile
기록 남기기

0개의 댓글