전체 선택, 더보기, 선택 삭제 구현에 대한 기록

cansweep·2022년 9월 29일
1
post-thumbnail

궁금해약 프로젝트는 사진을 통해 약의 정보를 제공하고 북마크한 약의 복약 알림을 받아볼 수 있는 서비스다.

최근에는 알림을 세팅하는 페이지와 받은 알림 기록들을 리스트로 보여주는 페이지를 구현하고 있는데 이 중 전체 선택, 더보기, 선택 삭제 기능을 구현하며 겪었던 트러블을 기록하고자 한다.

전체 선택

구현

먼저 전체 선택 로직을 설명하자면 아래와 같다.

1. 전체 선택 checkbox의 checked가 true라면 모든 message의 id를 저장한다.
2. 전체 선택 checkbox의 checked가 false라면 빈 배열을 저장한다.

전체 선택 기능은 모든 알림을 선택해 지우고 싶을 경우를 위해 둔 기능이다.
따라서 전체 선택 버튼을 누르는 경우는 모든 알림을 지우고 싶다는 것과 같으니 삭제할 알림의 id를 저장할 필요가 있었다.

const [selectedMessagesId, setSelectedMessagesId] = useState<string[]>([]);

const selectAllMessageHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSelectedMessagesId(
      e.target.checked ? messages.map((message) => message.id) : [],
    );
  };

각 알림의 id를 저장하기 위해 selectedMessagesId라는 string[] 타입의 state를 생성했고 빈 배열로 초기화했다.
그리고 전체 선택 checkbox를 누를 때마다 input의 checked 상태를 확인한 뒤 true라면 모든 알림의 id로, false라면 빈 배열로 selectedMessagesId의 값을 바꾸어 주었다.

<input
   type="checkbox"
   id={message.id}
   name="messages"
   checked={selectedMessagesId.includes(message.id)}
   onChange={selectMessageHandler}
/>

이때 각 알림 checkbox의 checked 상태는 selectedMessagesId에 해당 알림의 id가 있는지를 확인하면 된다.

 const selectMessageHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { checked, id } = e.target;

    if (checked) {
      setSelectedMessagesId((cur) => [...cur, id]);
    } else {
      setSelectedMessagesId((cur) =>
        cur.filter((messageId) => messageId !== id),
      );
    }
  };

또한 개별 선택의 경우에는 checked 여부에 따라 id를 selectedMessagesId에 추가하거나 filter를 사용해 걸러준다.

문제

구현 중 이런 에러가 떴었는데 이 경고를 해석해보면 checked prop을 onChange 없이 사용했다고 한다.

해결

전체 선택 기능을 구현할 때 개별 선택은 구현하지 않고 전체 선택 기능부터 구현하기 시작했는데 각 알림 checkbox의 checked 상태가 이 input의 onChange 핸들러에 의해 결정되는 것이 아니라 전체 선택 input의 onChange 핸들러에 의해 결정되어서 경고를 주는 것이다.

따라서 이 경고는 개별 선택 checkbox에 selectMessageHandler라는 onChange 핸들러를 추가해서 해결했다.

나중에 전체 선택을 구현할 일이 있다면 개별 선택 먼저 구현하고 전체 선택을 구현하는 것이 좋을 것 같다.

더보기

구현

위 예시에서는 예시를 위해 2개씩만 추가하고 있지만 실제 기능은 10개씩 알림 목록을 렌더링한다.
즉, 초기 렌더링에서는 10개만을 보여주고 더보기 버튼을 누를 때마다 목록이 10개씩 늘어난다.

더보기 버튼(pagination)의 로직은 아래와 같다.

1. 초기에는 1페이지를 렌더링한다.
2. 버튼을 누를 때마다 2, 3, ..., n 페이지의 알림 목록을 가져온다.
3. 이전의 알림 목록과 새로 받아온 알림 목록을 함께 보여준다.

더보기 버튼을 누를 때 중요한 것은 이전 페이지의 알림 목록을 새로운 알림 목록으로 대체하는 것이 아니라 이전 페이지의 알림 목록 + 새로운 페이지의 알림 목록을 보여주는 것이다.

const [messages, setMessages] = useState<MessageValues[]>([]);
const [pageCount, setPageCount] = useState(1);

useQuery(
    ["getMessages", pageCount],
    () => Api.get<MessageResponse>(`/alarms/${pageCount}`),
    {
      retry: false,
      refetchOnWindowFocus: false,
      onSuccess: ({ alarms }) => {
        setMessages((prev) =>[...prev, ...alarms]);
      },
    },
  );

따라서 page 수를 저장할 pageCount라는 number 타입의 state를 생성했고 1로 초기화했다.
그리고 더보기 버튼을 누를 때마다 1씩 더해주고 해당 페이지의 알림 목록을 가져온다.
마지막으로 알림 목록을 저장해둔 messages라는 state에 이전 목록 + 새로운 알림 목록을 저장한다.

문제

궁금해약 프로젝트의 모든 api는 react-query를 사용한다.
알림 목록을 가져오는 api도 useQuery를 사용하였는데 windowFocus나 retry가 활성화될 경우 같은 페이지의 알림 목록을 여러번 불러오는 이슈가 생겼다.

state를 새로 갈아끼운다면 문제는 없지만 더보기 기능을 구현하면서 이전 state에 새로운 알림 목록을 더하도록 했기 때문에 알림 목록에 중복이 생길 수밖에 없었다.

따라서 일단 retry나 refetchOnWindowFocus 기능을 false 상태로 두었다.
하지만 이외에도 중복이 생길 수 있는 경우는 많기 때문에 아예 messages에서 중복을 제거하는 것이 필요했다.

해결

onSuccess: ({ alarms }) => {
   setMessages((prev) =>_.uniqBy([...prev, ...alarms], "id");
},

중복 제거는 lodash의 uniqBy 함수를 사용했는데 이전 목록과 새로 받아온 목록을 합치면서 알림의 id를 기준으로 중복이 되는 값들을 없애고 결과로 반환된 배열을 messages에 넣어주었다.

선택 삭제

구현

사실 선택 삭제 기능만 두고 보면 큰 문제는 없다.
선택된 알림의 id를 저장해둔 state인 selectedMessagesId의 값들을 백엔드가 원하는 형태로 삭제하라고 보내주면 되기 때문이다.

여기서 중요한 점은 삭제한 이후 알림 목록을 갱신해야 한다는 점이다.

const deleteMessagesMutation = useMutation(
    (data: deleteMessageValues) =>
      Api.post<CommonResponse, deleteMessageValues>("/alarms/delete", data),
    {
      onSuccess: () => {
        queryClient.invalidateQueries(["getMessages", pageCount]);
      },
      onError: (err) => {
        console.log(err);
      },
    },
  );

따라서 useMutation을 사용하여 삭제할 알림들의 id를 백엔드로 전달하고 삭제가 성공했을 경우 이전에 알림 목록을 받아오며 사용했던 쿼리 키인 ["getMessages", pageCount]를 무효화시켜주었다.

문제

하지만 새로 알림 목록을 갱신하며 문제가 생겼다.
더보기 버튼을 구현하면서 알림 목록 갱신 시 이전 알림 목록 + 새로운 알림 목록으로 렌더링하게 되어있는데 이때문에 삭제된 알림이 messages에 남아있어 그대로 목록에 보여지는 것이다.

더보기 버튼을 구현하기 위해서는 이전 알림 목록이 필요하지만 삭제 기능을 위해서는 이전 알림 목록에서 삭제된 알림은 필요없었다.

해결

onSuccess: ({ alarms }) => {
  setMessages((prev) =>
    _.uniqBy([...prev, ...alarms], "id")
    .filter((message) => !selectedMessagesId.includes(message.id)),
  );
},

어떻게 할지 제일 많이 고민한 부분이었는데 삭제할 알림들의 id를 저장해두었다는 것에서 힌트를 얻었다.
삭제한 이후 목록을 갱신할 때 selectedMessagesId에 id가 존재하는 경우에는 messages에서 필터링해주었다.
만약 초기 렌더링이라면 selectedMessagesId가 빈 배열이니 필터링될 것이 없고 삭제한 이후라면 삭제된 알림의 id가 들어있으니 message를 저장할 때 걸러낼 수 있었다.

profile
하고 싶은 건 다 해보자! 를 달고 사는 프론트엔드 개발자입니다.

0개의 댓글