# Get Selector - **Jotai 라이브러리**에서 **get function**을 통해 다른 atom 값들을 가공해 return 할 수 있다. - **get**을 사용하면 연관된 State를 여러 개로 저장하지 않고 하나의 State로 관리할 수 있다. ```jsx export const minutesState = atom(0); // minutesState 값을 가져와서 Hour로 바꿔줌 export const hourSelector = atom((get) => { const minutes = get(minutesState); return minutes / 60; }); ``` # Set Selector - **set function**을 통해 다른 State의 값을 수정할 수 있다. ```jsx export const hourSelector = atom( // get selector (get) => { const minutes = get(minutesState); return minutes / 60; }, // set selector (get, set, newMinutes: number) => { set(minutesState, newMinutes); } ); ``` - **set function**은 일반 Atom과 동일하게 **`useSetAtom`**이나 `useAtom`으로 불러와 사용할 수 있다. ```jsx const [hours, setHours] = useAtom(hourSelector); const onHoursChange = (event: React.FormEvent<HTMLInputElement>) => { setHours(Number(event.currentTarget.value) * 60); }; <input value={hours} onChange={onHoursChange} type="number" placeholder="Hours" /> ``` ## Drag and Drop - 강의에서는 **react-beautiful-dnd**을 사용하지만 이는 현재 지원이 중단된 라이브러리임으로 `react-beautiful-dnd`를 계승하는 `@hello-pangea/dnd` 라이브러리로 대체하였다. ```powershell npm install @hello-pangea/dnd ``` ![[DragDropContext.png]] - `@hello-pangea/dnd` 에서는 **DragDropContext**와 **Droppable, Draggable** 총 3개의 컴포넌트를 사용한다. ## DragDropContext - 드래그 앤 드롭을 가능하게 하고자 하는 영역을 의미한다. - **DragDropContext**는 **onDragEnd function**과 **children**을 반드시 가지고 있어야 한다. ## Droppable - 무언가를 **Drag and Drop** 할 수 있는 영역을 의미한다. - _**droppableId**_라는 prop를 반드시 가지고 있어야 한다. (여러 개의 droppable이 존재 할 수 있기 때문에) - **Droppable** 또한 children을 가지고 있어야 하는데, children은 **함수**여야 한다. - 함수의 첫 번째 Props로 `*provided`* 를 가지고 있는데 안의 html 컴포넌트 안에 ref를 **provided.innerRef**로 설정한다. ```jsx <Droppable droppableId="board"> {(provided) => ( <ul ref={provided.innerRef} {...provided.droppableProps}> </ul> )} </Droppable> ``` ## Draggable - **Droppable** 영역 안에서 **Drag** 하는 영역을 의미한다. - _**draggableId**_라는 prop를 반드시 가지고 있어야 한다. (여러 개의 droppable이 존재 할 수 있기 때문에) - **Droppable**과 마찬가지로 **함수** children을 가지고 있어야 한다. - 함수의 첫 번째 Props로 `*provided`* 를 가지고 있는데 안의 html 컴포넌트 안에 ref를 **provided.innerRef**로 설정한다. - **Drag**가 가능하도록 하는 html 컴포넌트 안에 **provided.draggableProps**를 Props로 넘겨주면 된다. - 또한 Drag를 Handling하기 위해 html 컴포넌트 안에 **provided.dragHandleProps**를 Props로 넘겨주면 된다. ```jsx <Draggable draggableId="1" index={0}> {(provided) => <li ref={provided.innerRef} {...provided.draggableProps}><span {...provided.dragHandleProps}>클릭</span>One</li>} </Draggable> <Draggable draggableId="2" index={1}> {(provided) => <li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>Two</li>} </Draggable> ``` 💡 **draggableId = 1**인 컴포넌트의 경우 **dragHandleProps**를 `<span>` 안에 넣어 놨기 때문에 `<span>` 컴포넌트를 통해서만 **drag**가 가능하고 **draggableId = 2**인 컴포넌트의 경우 dragHandleProps를 `<li>` 컴포넌트 안에 넣어놨기 때문에 `<li>` 컴포넌트를 통해 Drag를 할 수 있다. ## OnDragEnd - OnDragEnd 함수는 Drag가 끝났을 때 여러 정보(어떤 **draggableId**를 가지고 있는 애를 어디로 옮겼는지)를 알려주는 Props를 가지고 있다. - _destination_ : Drag 끝낸 지점의 정보 - _source_ : Drag 시작 지점의 정보 - _draggableId_ : Drag한 item의 정보 - 이를 통해 상태 관리 라이브러리를 이용해서 **draggable**의 정보를 관리해주면 실제 **Drag and Drop**을 구현할 수 있다. ```jsx const onDragEnd = ({destination, source, draggableId}: DropResult) => { if (!destination) return; setTrelloToDos( oldTodos => { const toDosCopy = [...oldTodos]; // 원래 있던 아이템 제거 const taskObj = toDosCopy[source.index]; toDosCopy.splice(source.index, 1); // 새로운 위치에 아이템 추가 toDosCopy.splice(destination.index, 0, taskObj); return toDosCopy; }); }; ``` 💡 splice(start, deleteCount, …item) : splice는 start 지점에서 deleteCount만큼 지우고 item을 해당 지점 사이에 넣는 함수이다. ## Drag and Drop **Optimization** - 기본적으로 `DragDropContext` 안에 있는 컴포넌트들의 State가 바뀌기만 하면 모든 컴포넌트가 계속해서 리랜더링이 일어난다. - 이를 방지하기 위해 **react memo**를 이용한다. 이를 통해 State가 변경된 컴포넌트만 리랜더링한다. ```jsx const DragabbleCard = ({ toDo, index }: IDragabbleCardProps) => { return ( <Draggable key={toDo} draggableId={toDo} index={index}> {(provided) => <Card ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>{toDo}</Card>} </Draggable> ) } export default React.memo(DragabbleCard); ``` ## Multi Boards Movement - 여러 개의 **Boards**의 **Drag and Drop**을 관리하기 위해서는 atom에 Boards를 상태를 관리한다. - destination droppableId과 source의 droppableId를 비교하여 여러 Boards의 움직임을 관리한다. ```jsx const onDragEnd = ({ destination, source, draggableId }: DropResult) => { if (!destination) return; // 같은 보드 내에서 이동 if (destination.droppableId === source.droppableId) { setTrelloToDos(allBoards => { const boardCopy = [...allBoards[source.droppableId]]; // 원래 있던 아이템 제거 boardCopy.splice(source.index, 1); // 새로운 위치에 아이템 추가 boardCopy.splice(destination.index, 0, draggableId); // 보드 복사본을 반환 return { ...allBoards, [source.droppableId]: boardCopy, }; }); } else { // 다른 보드로 이동 setTrelloToDos(allBoards => { const sourceBoard = allBoards[source.droppableId]; const destinationBoard = allBoards[destination.droppableId]; // 원래 있던 아이템 제거 sourceBoard.splice(source.index, 1); // 새로운 위치에 아이템 추가 destinationBoard.splice(destination.index, 0, draggableId); // 보드 복사본을 반환 return { ...allBoards, [source.droppableId]: sourceBoard, [destination.droppableId]: destinationBoard, }; }); } }; ``` ## Droppable Snapshot - **Droppable과 Draggable**의 `Snapshot` 을 통해 Dr**ag**하고 있는 상태의 컴포넌트의 Style을 설정해줄 수 있다. - **Droppable Snapshot pors** - **isDraggingOver** : 해당 컴포넌트가 현재 **Droppable**에 있는지 알려주는 값 - **draggingFromThisWith** : User가 해당 board로부터 드래그를 시작했는지 알려주는 값 - **draggingOverThisWith** : User가 해당 board로부터 드래그가 끝났는지 알려주는 값 - **Draggable Snapshot pors** - **isDragging** - **isDropAnimating** - **dropAnimation** - …. ```jsx // **Droppable Snapshot** interface IAreaProps { isDraggingOver: boolean; isDraggingFromThis: boolean; } const Area = styled.div<IAreaProps>` background-color: ${props => props.isDraggingOver ? "lightgray" : props.isDraggingFromThis ? "lightgreen" : "transparent"}; flex-grow: 1; transition: background-color 0.3s ease-in-out; padding: 20px; `; <Area isDraggingOver={snapshot.isDraggingOver} isDraggingFromThis={Boolean(snapshot.draggingFromThisWith)} ref={provided.innerRef} {...provided.droppableProps}> {toDos.map((toDo, index) => ( <DragabbleCard key={toDo} toDo={toDo} index={index} /> ))} {provided.placeholder} </Area> // **Draggable Snapshot** interface ICardProps { isDragging: boolean; } const Card = styled.div<ICardProps>` padding: 10px 10px; background-color: ${props => props.isDragging ? "#74b9ff" : props.theme.cardColor}; border-radius: 5px; margin-bottom: 5px; box-shadow: ${props => props.isDragging ? "0px 2px 5px rgba(0, 0, 0, 0.5)" : "none"}; transition: box-shadow 0.3s ease-in-out; `; <Draggable key={toDo} draggableId={toDo} index={index}> {(provided, snapshot) => <Card ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} isDragging={snapshot.isDragging}>{toDo}</Card>} </Draggable> ``` ## Refs - **useRef**를 이용해 HTML 요소를 지정하고, 가져올 수 있다. - 다시 말해, **JS**로부터 HTML 요소를 가져오고 수정하는 방법이다. ```jsx import { useRef } from "react"; const inputRef = useRef<HTMLInputElement>(null); const onClick = () => { inputRef.current?.focus(); }; <input placeholder="grab me" ref={inputRef} /> <button onClick={onClick}>click me</button> ``` ## Editor Drag and Drop - Notion처럼 Editor에 Drag and Drop 기능이 있으면 좋을 것 같다고 생각해 Editor 기능을 구현하는 `Slate` 라이브러리에 `@hello-pangea/dnd` 라이브러리를 결합하여 구현하려고 시도했다. - `Slate` 안의 컴포넌트를 내가 원하는대로 구현하기가 힘들었다. - `@hello-pangea/dnd` 의 Draggable 컴포넌트에 안에 ___contentEditable_={true}, _suppressContentEditableWarning__ 를 통해 각 컴포넌트 안의 값을 넣을 수 있도록 구현했다._ - 또한 Draggable 컴포넌트에 안에 _**onBlur, onInput, onKeyDown**_ function으로 값이 변할 때마다 State가 변하도록 구현했다. - Function ```jsx const UpdatePostElement = (text: string) => { setPostElements(prev => { const newContent: PostAtom = { type: "paragraph", content: text, }; prev.splice(index, 1); prev.splice(index, 0, newContent); return prev; }); } const handleBlur = () => { if (contentRef.current && contentRef.current.textContent?.trim() === '') { handleDelete(); } }; const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => { if (e.key === 'Backspace' && contentRef.current?.textContent?.trim() === '') { e.preventDefault(); handleDelete(); } if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); const currentText = contentRef.current?.innerHTML .replace(/<div>/g, '\\n') .replace(/<\\/div>/g, '') .replace(/<br>/g, '\\n') || ''; // 현재 텍스트를 커서 위치 기준으로 분할 //const cursorPosition = window.getSelection()?.anchorOffset || 0; // const beforeCursorContent = currentText.slice(0, cursorPosition); UpdatePostElement(currentText); // 새 요소 추가 setPostElements(prev => [...prev, { type: "paragraph", content: "" }]); // 다음 요소에 포커스 setTimeout(() => { const nextElement = document.querySelector(`[data-index="${index + 1}"]`); if (nextElement instanceof HTMLElement) { nextElement.focus(); } }, 0); } }; const handleInput = (e: React.ChangeEvent<HTMLDivElement>) => { UpdatePostElement(e.target.innerText); }; ``` - Draggable Component ```jsx <Draggable draggableId={`post-${index}`} index={index}> {(provided) => ( <Wrapper ref={provided.innerRef} {...provided.draggableProps}> {element.type === "paragraph" ? ( <PostContent ref={contentRef} contentEditable={true} suppressContentEditableWarning onBlur={handleBlur} onInput={handleInput} onKeyDown={handleKeyDown} > {element.content} </PostContent> ) : element.type === "image" ? ( <ImageContainer> <Image src={element.url as string} alt={element.content} width="auto" height="auto" style={{ maxWidth: "100%", maxHeight: "100%" }} $objectFit="contain" /> <DeleteButton onClick={handleDelete}> <Delete_icon fill="#909090" width="72px" height="56px" /> </DeleteButton> </ImageContainer> ) : null} <MoveButton {...provided.dragHandleProps}>::</MoveButton> </Wrapper> )} </Draggable> ``` ![[Post-Editor-Drag-and-Drop-시연 1.mp4]]