React에서 드래그 앤 드롭을 구현하는 방법

소개



오늘 기사에서는 Trello 또는 ClickUp과 같은 애플리케이션과 유사한 인터페이스를 만들 것입니다. 아이디어는 앱의 기능을 확장할 수 있도록 기반을 만드는 것입니다.

기사의 끝에서 다음과 유사한 결과를 얻을 수 있습니다.



우리는 무엇을 사용할 것입니까?



오늘 우리는 다음과 같은 평범하지 않은 많은 도구를 사용하지 않을 것입니다.

  • Stitches - 경이로운 개발 경험이 있는 css-in-js 스타일링 라이브러리

  • radash - 문자열, 개체 및 배열을 처리하는 데 도움이 되는 일련의 함수를 제공하는 유틸리티 라이브러리

  • @dnd-kit/core - dnd를 구현하는 데 사용할 라이브러리입니다. 직관적이고 가벼우며 블록의 새로운 아이입니다
  • .

    이들은 이 문서에서 사용된 라이브러리이지만 동일한 결과를 다른 라이브러리에서도 쉽게 복제할 수 있다는 점을 명심하십시오.

    전제 조건



    이 자습서를 따르려면 다음이 필요합니다.
  • React 기본 이해
  • TypeScript에 대한 기본 이해

  • TypeScript를 몰라도 아무 문제가 없을 것입니다. 항상 데이터 유형을 "무시"할 수 있지만 오늘의 예에서는 전체 프로세스가 훨씬 쉬워집니다.

    시작하기



    첫 번째 단계로 프로젝트 디렉토리를 생성하고 해당 디렉토리로 이동합니다.

    yarn create vite react-dnd --template react-ts
    cd react-dnd
    


    이제 필요한 종속성을 설치할 수 있습니다.

    yarn add radash @dnd-kit/core @stitches/react @fontsource/anek-telugu
    


    상용구가 생성되면 애플리케이션 작업을 시작할 수 있지만 필요한 구성 요소를 만들기 전에 몇 가지 사항에 주의를 기울여야 합니다.

    먼저 오늘 예제의 두 가지 중요한 구성 요소의 기초를 형성할 두 가지 프리미티브를 만들어 보겠습니다. 첫 번째 프리미티브는 Droppable.tsx 이며 기본적으로 드래그할 수 있는 여러 요소를 포함하는 영역입니다.

    // @/src/primitives/Droppable.tsx
    import { FC, ReactNode, useMemo } from "react";
    import { useDroppable } from "@dnd-kit/core";
    
    interface IDroppable {
      id: string;
      children: ReactNode;
    }
    
    export const Droppable: FC<IDroppable> = ({ id, children }) => {
      const { isOver, setNodeRef } = useDroppable({ id });
    
      const style = useMemo(
        () => ({
          opacity: isOver ? 0.5 : 1,
        }),
        [isOver]
      );
    
      return (
        <div ref={setNodeRef} style={style}>
          {children}
        </div>
      );
    };
    


    우리가 필요로 할 다른 프리미티브는 드래그할 수 있는 각 요소에 사용될 Draggable.tsx입니다.

    // @/src/primitives/Draggable.tsx
    import { FC, ReactNode, useMemo } from "react";
    import { useDraggable } from "@dnd-kit/core";
    
    interface IDraggable {
      id: string;
      children: ReactNode;
    }
    
    export const Draggable: FC<IDraggable> = ({ id, children }) => {
      const { attributes, listeners, setNodeRef, transform } = useDraggable({ id });
    
      const style = useMemo(() => {
        if (transform) {
          return {
            transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
          };
        }
        return undefined;
      }, [transform]);
    
      return (
        <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
          {children}
        </div>
      );
    };
    


    우리가 생성한 두 프리미티브에 id라는 공통 소품이 있음을 알아차렸을 것입니다. 분명히 그들은 식별자, 다른 사용 사례를 참조합니다.
  • Droppable.tsx의 경우 ID는 작업의 단계(백로그, 진행 중 등)에 해당합니다.
  • Draggable.tsx에서는 요소 id에 해당합니다(정수 또는 uuid일 수 있음).

  • 이제 프리미티브가 있으므로 이를 사용할 구성 요소에서 작업할 수 있습니다. DraggableElement.tsx라는 가장 간단한 구성 요소부터 시작하여 작업 콘텐츠 렌더링을 담당합니다.

    // @/src/components/DraggableElement.tsx
    import { FC, useMemo } from "react";
    import { styled } from "@stitches/react";
    
    import { Draggable } from "../primitives";
    
    interface IDraggableElement {
      identifier: string;
      content: string;
    }
    
    export const DraggableElement: FC<IDraggableElement> = ({
      identifier,
      content,
    }) => {
      const itemIdentifier = useMemo(() => identifier, [identifier]);
    
      return (
        <Draggable id={itemIdentifier}>
          <ElementWrapper>
            <ElementText>{content}</ElementText>
          </ElementWrapper>
        </Draggable>
      );
    };
    
    const ElementWrapper = styled("div", {
      background: "#f6f6f6",
      borderRadius: 10,
      height: 120,
      width: "100%",
      display: "flex",
      justifyContent: "center",
      alignItems: "center",
      marginTop: 12,
    });
    
    const ElementText = styled("h3", {
      fontSize: 18,
      fontWeight: 600,
    });
    


    우리가 만들 마지막 구성 요소는 Column.tsx 이며 특정 작업과 관련된 각 요소를 렌더링하는 역할을 합니다.

    이 구성요소에 대해 말하자면 <DropPlaceholder />라는 매우 중요한 요소가 있는데 바로 센서, 즉 잡고 있는 요소가 특정 컬럼/스테이지로 가는지를 감지하는 요소입니다.

    // @/src/components/Column.tsx
    import { FC, useMemo } from "react";
    import { styled } from "@stitches/react";
    import * as _ from "radash";
    
    import { Droppable } from "../primitives";
    import { DraggableElement } from "./DraggableElement";
    
    export interface IElement {
      id: string;
      content: string;
      column: string;
    }
    
    interface IColumn {
      heading: string;
      elements: IElement[];
    }
    
    export const Column: FC<IColumn> = ({ heading, elements }) => {
      const columnIdentifier = useMemo(() => _.camal(heading), [heading]);
    
      const amounts = useMemo(
        () => elements.filter((elm) => elm.column === columnIdentifier).length,
        [elements, columnIdentifier]
      );
    
      return (
        <ColumnWrapper>
          <ColumnHeaderWrapper variant={columnIdentifier as any}>
            <Heading>{heading}</Heading>
            <ColumnTasksAmout>{amounts}</ColumnTasksAmout>
          </ColumnHeaderWrapper>
          <Droppable id={columnIdentifier}>
            {elements.map((elm, elmIndex) => (
              <DraggableElement
                key={`draggable-element-${elmIndex}-${columnIdentifier}`}
                identifier={elm.id}
                content={elm.content}
              />
            ))}
            <DropPlaceholder />
          </Droppable>
        </ColumnWrapper>
      );
    };
    
    const Heading = styled("h3", {
      color: "#FFF",
    });
    
    const ColumnWrapper = styled("div", {
      width: 320,
      padding: 10,
      border: "dashed",
      borderWidth: 2,
      borderRadius: 10,
    });
    
    const DropPlaceholder = styled("div", {
      height: 35,
      backgroundColor: "transparent",
      marginTop: 15,
    });
    
    const ColumnHeaderWrapper = styled("div", {
      display: "flex",
      flexDirection: "row",
      justifyContent: "space-between",
      alignItems: "center",
      variants: {
        variant: {
          backlog: {
            background: "#F94892",
          },
          inProgress: {
            background: "#5800FF",
          },
          inReview: {
            background: "#ffb300",
          },
          done: {
            background: "#24A19C",
          },
        },
      },
      padding: "0px 10px 0px 10px",
      borderRadius: 10,
    });
    
    const ColumnTasksAmout = styled("span", {
      display: "flex",
      justifyContent: "center",
      alignItems: "center",
      width: 30,
      height: 30,
      borderRadius: 6,
      color: "#FFF",
      background: "rgba( 255, 255, 255, 0.25 )",
      boxShadow: "0 8px 32px 0 rgba( 255, 255, 255, 0.18 )",
      backdropFilter: "blur(5px)",
      border: "1px solid rgba( 255, 255, 255, 0.18 )",
    });
    


    마지막으로 App.tsx에서 필요한 종속성을 가져오고 앱에 포함할 단계와 관련된 일부 변수 및 상태를 정의합니다.

    그런 다음 드래그한 요소와 관련된 데이터를 인수로 수신하는 handleOnDragEnd()라는 함수를 만들어야 합니다. 상태를 업데이트하기 위해(예: inProgress 단계를 inReview로 변경).

    마지막으로 각 단계에 해당하는 각 열을 매핑할 수 있으며 이러한 각 열의 소품에서 이와 관련된 요소를 전달할 수 있습니다.

    import "@fontsource/anek-telugu";
    import { useCallback, useState } from "react";
    import { DndContext, DragEndEvent } from "@dnd-kit/core";
    import { styled } from "@stitches/react";
    import * as _ from "radash";
    
    import { Column, IElement } from "./components";
    
    const COLUMNS = ["Backlog", "In Progress", "In Review", "Done"];
    export const DEFAULT_COLUMN = "backlog";
    
    const DEFAULT_DATA_STATE: IElement[] = [
      {
        id: _.uid(6),
        content: "Hello world 1",
        column: DEFAULT_COLUMN,
      },
      {
        id: _.uid(6),
        content: "Hello world 2",
        column: DEFAULT_COLUMN,
      },
    ];
    
    export const App = () => {
      const [data, setData] = useState<IElement[]>(DEFAULT_DATA_STATE);
    
      const handleOnDragEnd = useCallback(
        ({ active, over }: DragEndEvent) => {
          const elementId = active.id;
          const deepCopy = [...data];
    
          const updatedState = deepCopy.map((elm): IElement => {
            if (elm.id === elementId) {
              const column = over?.id ? String(over.id) : elm.column;
              return { ...elm, column };
            }
            return elm;
          });
    
          setData(updatedState);
        },
        [data, setData]
      );
    
      return (
        <DndContext onDragEnd={handleOnDragEnd}>
          <MainWrapper>
            {COLUMNS.map((column, columnIndex) => (
              <Column
                key={`column-${columnIndex}`}
                heading={column}
                elements={_.select(
                  data,
                  (elm) => elm,
                  (f) => f.column === _.camal(column)
                )}
              />
            ))}
          </MainWrapper>
        </DndContext>
      );
    };
    
    const MainWrapper = styled("div", {
      display: "flex",
      justifyContent: "space-evenly",
      backgroundColor: "#fff",
      paddingTop: 40,
      paddingBottom: 40,
      fontFamily: "Anek Telugu",
      height: "90vh",
    });
    


    결론



    늘 그렇듯이 기사가 마음에 드셨기를 바라며 기존 프로젝트에 도움이 되었거나 단순히 사용해 보고 싶으셨기를 바랍니다.

    기사에서 잘못된 부분을 발견했다면 댓글로 알려주시면 수정하겠습니다. 마치기 전에 이 기사의 소스 코드에 액세스하려면 github 저장소에 대한 링크here를 남겨둡니다.

    좋은 웹페이지 즐겨찾기