logo
github
UX를 고려한 필터링 기능 직접 구현해 보기
June 30, 2023

1. 개요

1) 기능 설명

아래 사진과같이 참여자의 이름으로 필터링 하는 기능이 필요하다!
따라서 개별 선택과 전체 선택 기능이 필요한데, 생각보다 고려해야 할 사항이 많아서 기록해 본다.

2) 요구사항

  1. 이미 전체 전체 선택된 상태에서 ‘전체선택’ 버튼을 한 번 더 누르면 전체 해제
  2. 참여자 이름을 하나씩 누르고, 모든 참여자 이름이 선택되었다면 전체 선택 버튼도 활성화하기
  3. 전체 선택 된 상태에서 참여자 이름을 눌러 전체 선택이 해제되면 전체 선택 버튼도 해제하기
  4. ‘적용하기’ 버튼을 누르지 않은 채로 바텀시트를 닫으면 이전 결과 그대로 나타내기
  5. 바텀시트를 다시 열었을 때 이전 선택 결과 불러오기

… 🥲

2. 개발 과정

1) 데이터 구조 잡기

참여자 목록을 객체 형태로 만들어 name에는 참여자의 이름을, 선택 여부는 isSelected로 판단한다.

// participatsList

[
  { "name": "김리더", "isSelected": false },
  { "name": "김주현", "isSelected": false }
]

2) 컴포넌트 구조 및 기능

  • 부모 컴포넌트: participantsListsetParticipatsList를 자식 컴포넌트로 넘김
  • 자식 컴포넌트: 부포 컴포넌트가 props로 내려 준 participantsList setParticipatsList를 가지고 필터링 기능 수행

3) 자식 컴포넌트 <Option />

스타일링 관련 코드는 생략!!

(0) 사용하는 State

불필요한 state 선언 없이 나름대로 이유 있게 선언하고 구현해 보았다.

먼저 전체 선택 여부를 파악할 수 있는 변수를 선언한다.

const [isAllSelected, setIsAllSelected] = useState<boolean>(false);

isAllSelected 값이 true 이면 전체선택이 된 상태이고, false인 경우는 전체해제 된 상태이다.

각 버튼들을 클릭할 때마다 isSelected 값을 변경하여 그 결과를 updatedList에 저장한다. 초기값은 participantsList로 설정하여 ‘전체선택’된 상태를 default로 한다.

const [updatedList, setUpdatedList] =
  useState<Participants[]>(participantsList);

participantList의 값을 직접 변경하지 않는 이유는, 하단의 ‘적용하기’ 버튼을 눌렀을 때에만 선택 결과가 반영되도록 하기 위함이다. 즉, 선택을 했더라도 ‘적용하기’ 버튼을 누르지 않고 모달창을 닫았다면 변화가 없는 것이다. 따라서 ‘적용하기’ 버튼을 클릭했을 때에만 participantList를 업데이트 한다.

몇 명이 선택되었는지 확인하기 위한 selectedCount 변수이다.

const selectedCount = updatedList.filter(
  ({ isSelected }: { isSelected: boolean }) => isSelected === true
).length;

(1) 전체 선택을 위한 함수 구현

다음으로 전체 선택을 위한 함수인 handleSelectAll을 작성한다. 전체 선택된 상태에서 전체선택 버튼을 한 번 더 클릭하면 전체 해제 된다.

const handleSelectAll = () => {
  const newList = updatedList.map((participant: Participants) => ({
    ...participant,
    isSelected: !isAllSelected,
  }));

  setUpdatedList(newList);
  setIsAllSelected(!isAllSelected);
};

(2) 참여자 블록 선택을 위한 함수 구현

참여자들의 이름이 적힌 개별 버튼들을 클릭할 때마다 실행되는 함수를 작성한다.

const handleBlockClick = (
  e: MouseEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>
) => {
  const target = e.target as HTMLDivElement;

  const newList = updatedList.map(({ name, isSelected }: Participants) =>
    name === target.id
      ? { name: name, isSelected: !isSelected }
      : { name: name, isSelected: isSelected }
  );

  setUpdatedList(newList);
};

(3) 선택 초기화 기능 구현

‘새로고침’ 버튼을 클릭하여 선택 항목을 초기화하는 함수를 작성한다.

const handleRefresh = () => {
  const newList = updatedList.map(({ name }: { name: string }) => ({
    name: name,
    isSelected: false,
  }));

  setUpdatedList(newList);
};

(4) 필터링 반영 기능 구현

‘적용하기’ 버튼을 클릭했을 때, 최종 결과를 participantsList에 반영해야 한다. 그래야지만 바텀시트를 닫았다 다시 열었을 때 이전에 선택한 항목들을 불러올 수 있다.

만약 사용자가 아무 버튼을 클릭하지 않고 ‘적용하기’ 버튼을 클릭했다면(selectedCount === 0), 자동으로 전체선택 되도록 한다.

const handleApplyClick = () => {
  if (selectedCount === 0) {
    const newList = updatedList.map((participant: Participants) => ({
      ...participant,
      isSelected: true,
    }));

    setParticipantsList(newList);
    setIsAllSelected(true);
  } else {
    setParticipantsList(updatedList);
  }

  setIsParticipantOpened(false); // 바텀시트 닫기
};

(5) 전체선택 버튼의 선택 여부

다음과 같은 경우를 생각해 볼 수 있다.

  • 전체 선택 후 참여자 이름을 하나 눌러 전체 선택이 해제된 경우

  • 참여자 이름을 하나씩 모두 눌러 결국 전체 선택이 된 경우

위와 같은 경우도 화면상으로 전체선택 버튼이 선택되었는지, 선택되지 않았는지 나타내기 위해서 아래와 같이 코드를 추가한다. 즉, 모든 참여자가 선택되었다면 전체선택 버튼을 활성화하고, 그렇지 않으면 활성화하지 않는다.

useEffect(() => {
  setIsAllSelected(selectedCount === updatedList.length);
}, [selectedCount, updatedList, setIsAllSelected]);

4. 코드 전문

아래는 자식 컴포넌트인 <Option /> 컴포넌트이다.

import { useState, MouseEvent, TouchEvent, useEffect } from 'react';

// 그외 import문 생략

const ParticipantsOption = ({
  setIsParticipantOpened,
  participantsList,
  setParticipantsList,
}: ParticipantsOptionTypes) => {
  const [isAllSelected, setIsAllSelected] = useState<boolean>(false);
  const [updatedList, setUpdatedList] =
    useState<Participants[]>(participantsList);

  const selectedCount = updatedList.filter(
    ({ isSelected }: { isSelected: boolean }) => isSelected === true
  ).length;

  const handleSelectAll = () => {
    const newList = updatedList.map((participant: Participants) => ({
      ...participant,
      isSelected: !isAllSelected,
    }));

    setUpdatedList(newList);
    setIsSelectedAll(!isAllSelected);
  };

  const handleBlockClick = (
    e: MouseEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>
  ) => {
    const target = e.target as HTMLDivElement;

    const newList = updatedList.map(({ name, isSelected }: Participants) =>
      name === target.id
        ? { name: name, isSelected: !isSelected }
        : { name: name, isSelected: isSelected }
    );

    setUpdatedList(newList);
  };

  const handleRefresh = () => {
    const newList = updatedList.map(({ name }: { name: string }) => ({
      name: name,
      isSelected: false,
    }));

    setUpdatedList(newList);
  };

  const handleApplyClick = () => {
    if (selectedCount === 0) {
      const newList = updatedList.map((participant: Participants) => ({
        ...participant,
        isSelected: true,
      }));

      setParticipantsList(newList);
      setIsAllSelected(true);
    } else {
      setParticipantsList(updatedList);
    }

    setIsParticipantOpened(false);
  };

  useEffect(() => {
    setIsAllSelected(selectedCount === updatedList.length);
  }, [updatedList, setIsAllSelected]);

  return (
    <>
      <Wrapper>
        <Block
          id="전체참여자"
          onClick={handleSelectAll}
          isSelected={isAllSelected}
        />
        {updatedList.map(({ name, isSelected }: Participants) => (
          <Block
            onClick={handleBlockClick}
            key={name}
            id={name}
            isSelected={isSelected}
          />
        ))}
      </Wrapper>
      <Bottom>
        <Refresh onClick={handleRefresh}>
          <RefreshIcon src={refresh} alt="refresh" />
          <RefreshButton>새로고침</RefreshButton>
        </Refresh>
        <Button onClick={handleApplyClick}>적용하기</Button>
      </Bottom>
    </>
  );
};

export default ParticipantsOption;

3. 회고(?)

다시 봐도 그렇게 어려운 로직은 아니다. 그래서 처음에 직접 구현해 봐야겠다고 생각했을 때 구현 과정이 엄청 간단할 줄 알았다. 그런데 점점 개발을 진행할수록 고려해야 하는 사항들이 늘어나서 생각했던 것보다 시간은 좀 더 걸렸지만, 요구사항들을 하나씩 클리어 해 나가는 게 재미있었다 ㅎㅎ 그리고 UX를 고려하여 구현하는 내 모습이 뿌듯했달까,, 😁 여러모로 재미있는 경험이었다!

corinthionia
🍀 Winning Mentality💭 능동적인 사고를 통한 성장을 위해 노력합니다
© Joohyun Kim (김주현) Corinthionia