# 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]]