프로그래머스 팀프로젝트: Drag and Drop ✨

박상하·2024년 5월 21일
1

Drag and Drop은 팀 프로젝트 주제를 선정할 때 나온 아이디어이다.
강의에 대한 스케줄링을 시간표를 이용한다면 더 나은 UI/UX를 위해 어떤 기능을 추가하면 좋을까
고민을 했고 그 결과 우리 팀은 Drag and Drop을 통해 시간표의 수정/삭제를 구현하기로 하였다.

먼저 라이브러리를 사용하지 않고 기본 React 문법을 이용하기로 하였다.
해당 기능을 맡게 되면서 어떻게 해당 위치에 대한 데이터를 집어넣을지 고민을 했다.

미리 짜여진 시간 테이블 위에 스케줄이 있다면 z-index를 조절하여 해당 시간 위에 수직 막대 형태의 스케줄 UI를 제공하기로 생각했다.

이를 구현하기 위해 고민했던 점과 어려웠던 점을 기록해보자.

위치 정보를 어떻게 표시할까? 🧐

시간 테이블이 있을 때 어떻게 해당 위치에 대한 정보를 넣을지 고민을 했다.

각각 table태그에 id를 주고 해당 id에 접근을 해야할까,
ref를 사용해야할까,
event.target에 대한 정보를 최대한 활용하는 방법은 없나 등 고민을 한 결과
다음과 같은 아이디어를 생각했다.

시간 테이블과 동일한 날짜에 스케줄이 있다면 그곳에서 스케줄 컴포넌트(스케줄 막대)를 그리자

코드 구성

그래서 코드는 이렇게 구성을 했다.


//TableContents 함수
export default function TableContents({ schedule, scheduledLectures }: Props) {
  
// 생략

  return (
    <>
      <tbody>
        {Array.from({ length: 18 }).map((_, hourIndex) => (
          <tr key={hourIndex}>
            <td>{`${hourIndex + 6}:00`}</td>
            {schedule[hourIndex].map((lecture, dayIndex) => (
              <td
                key={`${dayIndex},${hourIndex}`}
                onDragOver={handleDragOver}
                onDrop={handleDrop(hourIndex + 6, dayIndex + 1)}
                title={`${dayIndex},${hourIndex}`}
              >
                <TableCell
                  scheduledLectures={
                    scheduledLectures === undefined ? [] : scheduledLectures
                  }
                  hourIndex={hourIndex}
                  dayIndex={dayIndex}
                />
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </>
  );
}
// TableCell.tsx

export default function TableCell({
  scheduledLectures,
  hourIndex,
  dayIndex,
}: Props) {
// 생략

  return (
    <>
      {scheduledLectures.map((lecture, i) => {
     return isCanPaintSchedule(lecture, dayIndex, hourIndex, dragingPoint) && (
            <TableCellStyled
              key={i}
              howlong={calculateHowLong(lecture)}
              startpoint={calculateStartPoint(lecture)}
              draggable={true}
              onDragStart={handleDragStartWrapper(lecture)}
              backgroundColor={randomColor(dayIndex)}
            >
              {lecture.title}
            </TableCellStyled>
          )
      })}
    </>
  );
}

const TableCellStyled = styled.div<TableCellProps>`
  width: 100%;
  position: absolute;
  background-color: ${(props) => props.backgroundColor};
  height: ${(props) => 50 * props.howlong}px;
  top: ${(props) => props.startpoint}%;
  right: 0;
  z-index: 999;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 11px;
`;

코드가 복잡하다..

일단 TableContents라는 컴포넌트는 시간 테이블이라고 보면된다.
해당 시간테이블 내부 각각의 시간 위치 위에 scheduledLectures 라는 스케줄링된 강의 데이터를 가져와서
해당 스케줄된 데이터가 갖는 값과 해당 시간 테이블 내부의 시간이 같다면 tableCell을 길이(전체 강의 길이)를 css에 적용하여 그려주는 방식이다.

이렇게 했을 때 아쉬운 점 🧐

이렇게 했을 때 아쉬운 점이 있다.

그럼 한 칸의 시간 데이터 마다 TableCell은 Map연산에 들어간다. 전체 시간 테이블은 6시~24시까지 각 1시간마다 td태그가 존재해 약 200개가 안되는 td가 존재한다. 그럼 각각 태그마다 해당 TableCell의 연산이 들어가다 보니 이는 굉장히 많은 연산을 수행하게된다.

그런데도 이 방법을 사용한 이유는 시간표 위에 해당 강의들의 시간을 수정할 때
기존 테이블이 기준이 되어야 하기 때문이다. TableComponents를 기준으로 해당 시간 정보를 가져올 것이기 때문에 (hourIndex,dayIndex를 통해) TableComponents와 TableCell을 분리하여 개발을 할 수 밖에 없었던 거 같다.

수정이 draggable로 이루어 지지 않는다면 컴포넌트간 깊이를 하나 더 두지 않고 TableComponents에서
scheduled가 된 데이터를 충분히 등록할 수 있었을 것이라 생각한다.

컴포넌트 구조가 복잡해졌다. 🧐

컴포넌트의 구조를 보면 다음과 같다.

Schedule
 ┣ MyLectureList.tsx
 ┣ MyTimeTable.tsx
 ┣ ScheduleModal.tsx
 ┣ Scheduling.tsx
 ┣ TableCell.tsx
 ┣ TableContainer.tsx
 ┣ TableContents.tsx
 ┗ TableHeader.tsx

큰 프로젝트에 비해 복잡하지 않을 수 있지만 프로젝트 규모에 비해 컴포넌트의 구조가 복잡해졌다고 느꼈다.
그래도 모듈화를 최대한 한거라고 생각했는데 문제는 Props에 있었다.
draggable을 위해서는 기본적으로 몇가지의 함수가 적용이 되어야했다.

handleDragStart
handleDragOver
handleDrop

여기에 추가적으로 모듈에 대한 open 여부를 갖는 state, 닫는 state, drag중인 데이터, delete를 위한 handleDrop, dragingPoint(현재 드래깅중인 위치 (hours,days index가 됨))

여기까지 보면 공유되는 계층은 다르겠지만 공유되어야 하는 props의 갯수가 8~9개가 되었다.

물론 해당 데이터가 필요로 하는 부분에서 props로 전달해주도록 할 수도 있지만
특정 컴포넌트는 5개 이상의 props를 전달받게되어서 해당 모습이 좋아보이지 않았다.

공유할 Props가 많아졌다 -> ContextAPI 사용 ✨

사실 이를 전역상태관리를 사용할지, Props를 사용할지, ContextAPI를 사용할지 고민을 했다.

그 중 ContextAPI를 사용한 이유는 해당 Provider내에서 공유되는 Props가 통일성이 있다고 판단했기 때문이다.

ContextAPI를 사용해보니 연관성이 짙은 props들은 하나의 ContextAPI를 사용하는 것도 좋다는 점을 느꼈다. 오히려 한 곳에서 관리를 하니 더 편했고 원하는 컴포넌트에서 불러다 쓰면되기 때문에 꽤 편리하다 느꼈다.


export const DragAndDropContext = createContext<State>(state);

export const DragAndDropProvider = ({ children }: Props) => {
  const [dropData, setDropData] = useState<UpdateProps>(drops);
  const [dropDataDelete, setDropDataDelete] =
    useState<DeleteProps>(dropsDelete);
  const [dragingPoint, setDragingPoint] = useState<number[]>([-1, -1]);
  const [isOpen, setIsOpen] = useState(false);
  
  const onOpen = () => {
    setIsOpen(true);
  };
  const onClose = () => {
    setIsOpen(false);![](https://velog.velcdn.com/images/tkdgk1996/post/f489fbec-f330-45bc-bec7-0a3818a01db8/image.gif)

  };
  
  const handleDragStart =
    (data: UpdateProps) => (event: React.DragEvent<HTMLDivElement>) => {
      event.dataTransfer?.setData("text/plain", JSON.stringify(data));
    };

  const handleDragOver = (
    event:
      | React.DragEvent<HTMLTableCellElement>
      | React.DragEvent<HTMLHeadElement>
  ) => {
    event.preventDefault();
    const dragPoint = event.currentTarget.title
      .split(`,`)
      .map((item) => Number(item));
    setDragingPoint(dragPoint);
  };

  const handleDrop =
    (startAt: number, dayIndex: number) =>
    (event: React.DragEvent<HTMLTableCellElement>) => {
      onOpen();
      event.stopPropagation();
      const getDropData = JSON.parse(event.dataTransfer.getData("text/plain"));
      getDropData.startAt = startAt;
      getDropData.weekDayID = dayIndex;
      setDropData(getDropData);
      setDragingPoint([-1, -1]);
    };

  const handleDropDelete = (event: React.DragEvent<HTMLHeadElement>) => {
    event.preventDefault();
    const getDropData = JSON.parse(event.dataTransfer.getData("text/plain"));
    setDropDataDelete(getDropData);
  };

  return (
    <DragAndDropContext.Provider
      value={{
        handleDragStart,
        handleDragOver,
        handleDrop,
        handleDropDelete,
        isOpen,
        onClose,
        dropData,
        dropDataDelete,
        dragingPoint,
      }}
    >
      {children}
    </DragAndDropContext.Provider>
  );
};

그런데 사용하기 편한만큼 단점도 있다. 한 곳에서 관리해서 좋긴 하지만 해당 파일의 복잡도는 올라간다.
한 눈에 봤을 때 가독성이 좋다고 느껴지지는 않는 거 같다.

이를 멘토님께 여쭤보니 멘토님은 그럴 수 있다고 하시고 멘토님께서는 ContextAPI보다
전역상태관리를 사용하는 것을 선호한다고 하셨다.

프로젝트의 크기가 커지게 되면 Provider들이 많아져 결국엔 복잡도가 올라간다는 말씀이셨다.

지금은 프로젝트의 크기가 작아 하나의 Provider의 복잡도만 올라갔지만 만약 규모가 크다면 전체적인 상태관리 라이브러리를 사용하는 게 더 가독성이 나을 수 있을 거 같다.

구현 결과 ✨

0개의 댓글

관련 채용 정보