[React] 선택/취소 기능: 1️⃣ 멋진 코드 짜려다 UX를 고려하지 못한 이야기

sjoleee·2022년 9월 17일
1
post-thumbnail
post-custom-banner

가능한 코드양은 적게, 선언형으로 멋지게 짜보려고 생각했으나 사용자 입장에서 불편한 UX가 되어버려 수정했던 이야기이다.

요구사항

초기 상태에서 상단 Card를 클릭할 경우

  • 클릭된 카드는 초록색으로 바뀜.
  • 하단 리스트에서 해당하는 카드 정보가 추가됨.

초록색으로 변경된 카드를 다시 클릭할 경우

  • 클릭된 카드는 흰색으로 바뀜.
  • 하단 리스트에서 해당하는 카드 정보가 제거됨.

하단 리스트의 항목을 클릭할 경우

  • 하단 리스트에서 클릭한 항목이 제거됨.
  • 상단 카드 리스트에서 해당하는 카드가 흰색으로 바뀜.

구현 방식(as-is)

  • 클릭된 카드의 isSelected: boolean!isSelected로 변경됨.
  • isSelected: true인 카드는 background-color: green 적용
  • filter 매서드를 사용해 isSelected: true인 카드들만으로 하단 Item들을 그려줌
  • Item을 클릭할 경우, isSelected: false로 변경

문제점

카드를 클릭한 순서대로 하단 리스트에 추가되지 않고, 상단 카드들의 순서에 따라 추가되는 문제가 있었다.

기능을 구현하면서 가장 신경썼던 부분이 효율적인 코드 그리고 선언형 매서드 사용이었다.
이전 프로젝트에서 가장 아쉬웠던 부분이 좋은 코드에 대한 고민이 부족한 상태에서 제대로 동작하는지만을 생각하며 개발했다는 점이었다.
이번 프로젝트에서는 '좋은 코드란 무엇일까?' 라는 생각을 계속 머리에 담아놓고 코드를 작성했다.
그러다 보니, 자꾸 짧고 '있어보이는' 코드만을 추구하게 된 것 같다.
그 사이드이팩트로... UX에 대한 생각을 못하게 됐달까... 결국 개발이라는게 내가 이따만큼 알아요를 뽐내는 것이 아니라, 유저에게 만족할만한 경험을 선사하는 것이 중요한게 아닐까 반성하게 되었다.

해결 방법

클릭된 시간을 기록하는 방법

처음 생각한 방법은 클릭된 시간을 기록하여 예전 순서대로 정렬해주는 방법이었다.
나쁘지 않았지만, 시간을 기록하는 것보다는 조금 더 일반적인 방법을 사용하자고 생각했다.

filter 대신 새로운 배열에 push하는 방법

결국 문제가 되는 부분은 하단 리스트이고, 클릭된 상단 카드를 배열에 담아서 보여주는 방식이라면 해결될 것이라고 생각했다.
그리고 시간을 기록하는 방법보다 더욱 일반적인 방법이라고 생각했다. 다른 사람이 내 코드를 봤을때 조금 더 친숙한 방법으로 구현되기를 원했기 때문에 이 방법을 택했다.

구현 방식(to-be)

참고로 zustand를 사용하고 있으며, 위에서 말한 배열은 전부 store의 state를 말하는 것이다.

//상단 카드의 onClick 함수
  const onMemberCardClick = () => {
    isSelected ? handleDeselect(id) : handleSelect(id);
  };
//handleSelect 함수
  const handleSelect = (id: number) => {
    isSelectedChange(); //클릭된 카드의 isSelected값을 변경해주는 함수
    members.forEach((item) =>
      item.id === id ? setSelectedMembers([...selectedMembers, item]) : null,
    ); //selectedMembers에 클릭된 카드를 추가해준다.
  };
//handleDeselect 함수
  const handleDeselect = (id: number) => {
    isSelectedChange(); //클릭된 카드의 isSelected값을 변경해주는 함수
    const newSelectedMembers = selectedMembers.filter((item) => item.id !== id);
    setSelectedMembers(newSelectedMembers); //selectedMembers에서 클릭된 카드만 제외한 newSelectedMembers를 만들고 set해준다.
  };
//isSelectedChange 함수
  const isSelectedChange = () => {
    const newMembers = [...members].map((item) =>
      item.id === id ? { ...item, isSelected: !item.isSelected } : item,
    ); //클릭된 카드의 isSelected를 변경한 newMembers를 생성
    setMembers(newMembers);
  };

여기까지가 상단 카드를 클릭하면 호출되는 함수들이다.
하단 리스트를 클릭할 경우는 추가가 없고, 삭제만 가능하다.
로직상 하단 리스트를 클릭할 경우 호출되는 함수는 handleDeselect를 그대로 사용하면 된다.

해결 실패

그러나, 원하는 대로 동작하지 않았다.
바로 set함수가 비동기로 동작하기 때문이었다

위쪽에서 카드를 선택하면, 아래쪽 리스트에 isSelectedtrue인 상태로 추가되어야 하는데, false인 상태로 추가되고 있었다.
그래서 handleDeselect를 해봤자... 이미 false인데 뭘 선택취소야? 라는 상황이 발생하는 것.

//handleSelect 함수
  const handleSelect = (id: number) => {
    isSelectedChange(); //클릭된 카드의 isSelected값을 변경해주는 함수
    members.forEach((item) =>
      item.id === id ? setSelectedMembers([...selectedMembers, item]) : null,
    ); //selectedMembers에 클릭된 카드를 추가해준다.
  };

handleSelect 함수에서 isSelectedChange 함수를 사용하여 isSelected값을 변경해주고 있다.

//isSelectedChange 함수
  const isSelectedChange = () => {
    const newMembers = [...members].map((item) =>
      item.id === id ? { ...item, isSelected: !item.isSelected } : item,
    ); //클릭된 카드의 isSelected를 변경한 newMembers를 생성
    setMembers(newMembers); //😡이녀석이 비동기로 동작한다!!!
  };

내가 생각한 대로라면,

  • 클릭한 카드의 isSelected가 변경된다.
  • 변경을 set함수로 members에 반영한다.
  • 변경이 반영된 members에서 클릭된 카드를 찾아내서 selectedMembers에 추가한다

이런 흐름으로 가야하는데, 실제로 동작하는 것은

  • 클릭한 카드의 isSelected가 변경된다.
  • 변경을 set함수로 members에 반영한다. 비동기로 동작하기 때문에 뒤에서 실행됨
  • 변경이 반영된 members에서 클릭된 카드를 찾아내서 selectedMembers에 추가한다
  • 미뤄놨던 set함수로 members가 업데이트된다.

엄밀히 말하자면 selectedMembers를 업데이트하기 전인데, 알아보기 쉽게 마지막으로 넣었다.
(selectedMembers에서 추가하고자 하는 item은 그 시점에서 아직 isSelected가 업데이트 되지 않은 상태이다.)

위 문제를 해결하는 내용은 다음 글에서 마저 기록하도록 하겠다!

profile
상조의 개발일지
post-custom-banner

0개의 댓글