anonymousRecords / touslesjours-admin

행복한 뚜둥이 생활을 위하여~🥖
https://touslesjours-admin.vercel.app/
0 stars 0 forks source link

[FIX] 샌드위치 스티커 색상을 변경했는데, 화면에 반영이 안 되는 이슈 #2

Open anonymousRecords opened 1 month ago

anonymousRecords commented 1 month ago

📌 문제 상황

change

드롭다운으로 데이터를 변경하면, 해당 데이터가 화면에 반영이 되지 않는 현상이 발생함.
해당 이슈는 드롭다운을 사용하는 냉판 페이지에서도 동일하게 발생하였고, 데이터 변경을 두 번 실행하면 적용되는 상황이었음.

anonymousRecords commented 1 month ago

📌 상황 분석

관련 커밋 1 관련 커밋 2

1. 수정 전 코드 관련 커밋

<Dropdown
   buttonContent={
   assignedWorkers.find((worker) => worker.date === dateofSelectedDate)
     ?.workers[0]
   }
   dropdownContent={dropdownContents}
/>
interface DropdownProps {
  buttonContent: React.ReactNode;
  dropdownContent: React.ReactNode[];
  onSelect: (selectedOption) => void;
}
...
const [selectedContent, setSelectedContent] = useState<React.ReactNode>(buttonContent);
<Dropdown
   buttonContent={dropdownData[columnIndex][rowIndex]} // buttonContent 부분에 <ul>, <li> 태그로 실제 구현하여 넘겨줌.
   onSelect={(index: number) => {
  handleDropDownChange(columnIndex, rowIndex, index);
  }}
/>

1.1. 드롭다운 컴포넌트 설계 우선 위처럼 구현을 하여, 드롭다운 컴포넌트를 구현하였다. 드롭다운 컴포넌트는 두 페이지(냉판, 샌드위치)에서 사용을 하였다.
따라서 공통 컴포넌트로 하여 구현할 계획이었다.

image image

그래서 어떻게 드롭다운 컴포넌트를 구현할지 설계를 먼저 진행하였다.

image image image

(출처 : https://ui.shadcn.com/docs/components/dropdown-menu)

(1) trigger에는 초기 값이 노출되어 보여짐. (2) trigger 클릭 시, 드롭다운이 펼쳐지면서 선택지들이 보여짐. (3) 선택지를 클릭하면, 해당 선택지로 trigger가 변경됨.

첫 번째로 공통 컴포넌트는 polymorphic한 것이 중요하다고 보았다. (참고 포스트)
따라서 선택지를 자유롭게 설정하는 것에 집중하였던 것 같다. 예를 들어 <ul>, <li> 태그로 구현할 수도 있지만, <div> 태그로도 할 수도 있고 자유롭게 두고 싶었다.
그리고 두 번째로 클릭한 선택지를 어떻게 확인할 지였다. trigger에 노출되는 값은 선택지 클릭에 따라 달라진다. 따라서 클릭 이벤트를 감지하고, 클릭한 값을 trigger에 반영을 어떻게 할지 고민하였다.

➕ 초반에는 이렇게 선택지에 div element를 보이게 하였다.

image

1.2. 초기 드롭다운 구현 위 고민을 거쳐 드롭다운을 구현하였다.

초기 드롭다운 구현 커밋

interface DropdownProps {
  buttonContent: React.ReactNode;
  dropdownContent: React.ReactNode[];
}

interface를 살펴보면, trigger에 노출되는 값을 뜻하는 buttonContent와 dropdownContent를 전달받도록 하였다.

그리고 드롭다운 내에서는 2개의 상태를 관리하도록 하였다.

const [selectedContent, setSelectedContent] = useState<React.ReactNode>(buttonContent);
const [isOpen, setIsOpen] = useState(false);

클릭한 선택지 상태를 관리하는 selectedContent와 드롭다운이 펼쳐지고 닫히는 상태를 관리하는 isOpen을 두었다.

trigger 역할을 하는 button 내에 selectedContent를 두어 선택지 변경에 따라 trigger에 노출되는 값을 변경하게 하였다. 그리고 isOpen이 true일 때만 div 태그가 노출되게 하여, 드롭다운이 펼쳐지고 닫히는 기능을 구현하였다. div 태그 내에는 prop으로 전달받은 dropdownContent를 map으로 보이게 하였고, 버튼을 클릭하면 handleContentSelect 함수를 통해 선택을 감지하게 하였다.

<button>
  {selectedContent}
</button>
{isOpen & (
  <div>
   {dropdownCotent.map((content, index) => (
    <button onClick={() => handleContentSelect(content)}>
       {content}
   </button>
  )}
  </div>
)}
)}

handleCotnentSelect 함수를 살펴보면 아래와 같다.

  const handleContentSelect = (content: React.ReactNode) => {
    setSelectedContent(content);
    toggleDropdown();
  };

전달받은 parameter를 selectedContent로 상태를 변경하고 toggleDropdown 함수를 호출하여 펼쳐진 드롭다운을 닫는 것이다.

이때까지만 해도, 드롭다운이 잘 펼쳐지고 닫히고, 무엇보다 클릭한 선택지로 드롭다운 trigger에 잘 반영이 되어 문제가 없다고 생각했다.

anonymousRecords commented 1 month ago

2. 첫 번째 수정 후 코드 참고 커밋

2.1. 새로운 이슈 냉판 담당자 페이지를 구현하면서, 새로운 문제에 봉착하였다.
냉판 담당자의 경우, 계산을 통해 냉판 담당자가 미리 정해진다.
해당 데이터를 supabase에 저장하고, 그 데이터를 가져와 드롭다운에 노출되게 하는 식으로 동작하는 것이다.

image

모달에 2개의 드롭다운이 들어가게 되었고, 첫 번째 드롭다운, 즉 담당자 1은 selectedWorker1로, 두 번째 드롭다운은 담당자 2 selectedWorker2로 상태를 관리하였다.

<Dropdown
   buttonContent={data.find((worker) => worker.date === selectedDate)?.workers[0]}
   dropdownContent={dropdownContents}
   onSelect={(buttonContent) => {
     setSelectedWorker1(buttonContent);
   }}
/>

따라서 buttonContent는 supabase에서 받아온 데이터에서 날짜에 맞는 담당자1을 찾아내고, 이를 초기 trigger 노출 값으로 지정하는 역할을 하고, dropdownContent는 선택지들 내용을 결정하고, 새로운 onSelect가 추가되었다.
onSelect는 드롭다운에 노출되는 값을 갈아끼우는 역할을 하게 되었다.

2.2. 드롭다운 수정

interface DropdownProps {
  buttonContent: React.ReactNode;
  dropdownContent: React.ReactNode[];
  onSelect: (selectedOption) => void;
}

...

const handleContentSelect = (content: React.ReactNode) => {
   setSelectedContent(content);
   onSelect(content);
   toggleDropdown();
};

드롭다운 컴포넌트에 onSelect를 추가해주었다.

anonymousRecords commented 1 month ago

3. 두 번째 수정 후 코드

관련 커밋

3.1. 새로운 이슈 해당 이슈를 생성한, 이슈를 발견하였다.
(1) 냉판 페이지에서 데이터 변경을 두 번 변경을 해야 화면에 적용됨. (2) 샌드위치 스티커를 변경해도, 화면에 반영이 되지 않음.

드롭다운과 관련있다고 생각하여 코드를 분석하였다.

값이 변경되어도, 렌더링이 발생하지 않아서 화면에 보이지 않는다고 생각했다.
따라서 아래와 같이 useEffect로 렌더링을 발생하게 하면 해당 이슈는 해결이 가능하다고 보았다.

useEffect(() => {
  setSelectedContent(buttonContent)
}, [buttonContent])

useEffect 훅을 사용하여 buttonContent가 변경될 때마다 setSelectedContent를 호출하여 selectedContent를 업데이트하게 하였다.

3.2. 드롭다운 컴포넌트 코드의 문제점 하지만 이슈 해결과 별개로 기존 코드에는 여러 문제가 존재한다고 보았다.

(1) React.ReactNode[]
자유롭게 dropdownContent를 두고 싶어서, React.ReactNode[] 타입을 지정하였다.
하지만 이 선택은 자유로움을 보장하는 것이 아닌, 사실상 타입을 지정하지 않는 것과 마찬가지인 선택이었다.

When to use JSX.Element vs ReactNode vs ReactElement?을 살펴보면, 아래와 같은 관계라고 정리할 수 있다.

ReactElement =< JSX.Element < ReactNode

React.ReactNode[] 타입을 사용하면 거의 모든 것을 허용한다는 장점을 가진다고 할 수 있지만, 다시 말하면 타입스크립트의 목적 중 하나는 코드의 안정성과 예측 가능성을 높이는 건데 해당 취지에는 부합하지 않는다고 보았다. 또한 ReactNode를 사용하면 리액트가 내부적으로 노드를 비교할 때 문제가 생길 수도 있다. 따라서 string[]으로 수정하여 구체적인 타입을 지정해주었다.

(2) onSelect vs selectedContent 선택지를 클릭하면, 변경을 반영하는 기능을 수행하는 존재가 필요하다. onSelect와 selectedContent 모두 해당 기능을 수행한다고 보았다.

조금 더 구체적으로 말해보면 다음과 같다.

공통 컴포넌트 내에서 상태를 관리하는 것에 대해 고민이 많았다.
독립적으로 상태를 관리할 수 있다는 장점을 지니고는 있으나, 확장성은 떨어진다고 보았다. 또 부모와 상태 공유하는데 어려움이 있다고 보았다. 따라서 드롭다운 컴포넌트의 경우, 컴포넌트 내부에서 상태를 지니는 것을 지양하고자 selectedContent을 없애고, onSelect만으로 선택지를 클릭하면, 변경을 반영하는 기능을 수행하려고 하였다.

  const handleContentSelect = (index) => {
    toggleDropdown();
    onSelect(index);
    setIsOpen(!isOpen);
    // setSelectedContent(dropdownContent[index]);
  };
...
            {dropdownContent.map((content, index) => (
              <button
                key={index}
                onClick={() => handleContentSelect(index)}
              >
                {content}
              </button>
            ))}

참고 커밋

// SandwichTable.tsx
                <Dropdown
                  buttonContent={dropdownData[columnIndex][rowIndex]}
                  dropdownContent={colorsContent}
                  onSelect={(index: number) => {
                    // console.log('index', index)
                    // setButtonNumber(index);
                    // console.log('buttonNumber', buttonNumber);
                    handleDropDownChange(columnIndex, rowIndex, index);
                  }}
                />

정리하자면, 더이상 상태를 공통 컴포넌트 내에서 관리하는 것이 아닌 부모 컴포넌트에서 관리하게 하였다.
따라서 onSelect로 부모 컴포넌트에서 클릭한 값을 index를 통해 공통 컴포넌트에 알려주고, index를 통해 드롭다운 컴포넌트를 노출시켰다. 또한 handleDropDownChange 함수를 부모 컴포넌트에서 호출해서 상태를 업데이트 및 관리하게 하였다.

anonymousRecords commented 1 month ago

➕ 추가 리팩토링 관련 커밋

interface DropdownProps {
  onSelect: (content: string) => void;
}

  const handleContentSelect = (index: string) => {
    toggleDropdown();
    onSelect(index);
  };

index 타입을 string로 명시해주었고, toggleDropdown();setIsOpen(!isOpen);이 중복되어서 중복을 없앴다.