6. TRELLO CLONE

Hapjeong Girl·2022년 10월 26일
0

MUTSA_STUDY

목록 보기
6/11
post-thumbnail

Trello 사이트를 클론 코딩 해보자

6.0 Get Selectors


selectorget을 이용해 Minutes를 입력하면 Hours가 자동으로 바뀌게 해보자.

  • 세팅하기
    • component 폴더 삭제
    • App.tsx 초기화
      function App() {
      	return <div></div>;
      }
      export default App;
    • atoms.tsx 초기화
      import { atom, selector } from 'recoil';
    • Index.tsx 세팅
      import React from 'react';
      import ReactDOM from 'react-dom';
      import App from './App';
      import { RecoilRoot } from 'recoil';
      import { createGlobalStyle, ThemeProvider } from 'styled-components';
      import { darkTheme } from './theme';
      
      const GlobalStyle = createGlobalStyle`
      	@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400&display=swap');
      
      	html, body, div, span, applet, object, iframe,
      	h1, h2, h3, h4, h5, h6, p, blockquote, pre,
      	a, abbr, acronym, address, big, cite, code,
      	del, dfn, em, img, ins, kbd, q, s, samp,
      	small, strike, strong, sub, sup, tt, var,
      	b, u, i, center,
      	dl, dt, dd, menu, ol, ul, li,
      	fieldset, form, label, legend,
      	table, caption, tbody, tfoot, thead, tr, th, td,
      	article, aside, canvas, details, embed,
      	figure, figcaption, footer, header, hgroup,
      	main, menu, nav, output, ruby, section, summary,
      	time, mark, audio, video {
      	margin: 0;
      	padding: 0;
      	border: 0;
      	font-size: 100%;
      	font: inherit;
      	vertical-align: baseline;
      	}
      	/* HTML5 display-role reset for older browsers */
      	article, aside, details, figcaption, figure,
      	footer, header, hgroup, main, menu, nav, section {
      	display: block;
      	}
      	/* HTML5 hidden-attribute fix for newer browsers */
      	*[hidden] {
      		display: none;
      	}
      	body {
      	line-height: 1;
      	}
      	menu, ol, ul {
      	list-style: none;
      	}
      	blockquote, q {
      	quotes: none;
      	}
      	blockquote:before, blockquote:after,
      	q:before, q:after {
      	content: '';
      	content: none;
      	}
      	table {
      	border-collapse: collapse;
      	border-spacing: 0;
      	}
      
      	*{
      		boxsizing: border-box;
      	}
      
      	body{
      		font-family: 'Source Sans Pro', sans-serif;
      		background-color: ${(props) => props.theme.bgColor};
      		color: ${(props) => props.theme.textColor}
      	}
      
      	a{
      		text-decoration: none;
      		color: inherit;
      	}
      `;
      
      ReactDOM.render(
      	<React.StrictMode>
      		<RecoilRoot>
      			<ThemeProvider theme={darkTheme}>
      				<GlobalStyle />
      				<App />
      			</ThemeProvider>
      		</RecoilRoot>
      	</React.StrictMode>,
      	document.getElementById('root')
      );
  1. App.tsx 에 input을 추가해주자

    function App() {
    	return (
    		<div>
    			<input type='number' placeholder='Minutes' />
    			<input type='number' placeholder='Hours' />
    		</div>
    	);
    }
    export default App;
  2. atoms.tsx 에 minuteState atom을 만들어주자

    import { atom, selector } from 'recoil';
    
    export const minuteState = atom({
    	key: 'minutes',
    	default: 0
    });
  3. App.tsx 에 input과 minuteState를 연결해주자

    import { useRecoilState } from 'recoil';
    import { minuteState } from './atom';
    
    function App() {
    	const [minutes, setMinutes] = useRecoilState(minuteState);
    	const onMinutesChange = (event: React.FormEvent<HTMLInputElement>) => {
    		setMinutes(parseInt(event.currentTarget.value));
    	};
    	return (
    		<div>
    			<input value={minutes} onChange={onMinutesChange} type='number' placeholder='Minutes' />
    			<input type='number' placeholder='Hours' />
    		</div>
    	);
    }
    export default App;

    💡 useRecoilState : atom의 값과 그 atom을 수정할 수 있는 함수를 돌려줌


    나만 오류나나?

    setMinutes(+event.currentTarget.value);로 했을 때 최종 결과에서 0이 무조건 붙음.

  4. atom.tsx 에 selector을 사용하자

    export const hourSelector = selector({
    	key: 'hours',
    	get: ({ get }) => {
    		const minutes = get(minuteState);
    		return minutes / 60;
    	}
    });
  5. App.tsx에 hourSelector의 값을 가져오자

    import { useRecoilState, useRecoilValue } from 'recoil';
    import { hourSelector, minuteState } from './atom';
    
    function App() {
    	const [minutes, setMinutes] = useRecoilState(minuteState);
    	const hours = useRecoilValue(hourSelector);
    	const onMinutesChange = (event: React.FormEvent<HTMLInputElement>) => {
    		setMinutes(+event.currentTarget.value);
    	};
    	return (
    		<div>
    			<input value={minutes} onChange={onMinutesChange} type='number' placeholder='Minutes' />
    			<input value={hours} type='number' placeholder='Hours' />
    		</div>
    	);
    }
    export default App;

완성!

6.1 Set Selectors


selectorset을 이용해 반대로 Hours를 입력해도 Minutes가 나오도록 만들어보자.

  1. atom.tsx에서 set함수에 접근해보자

    export const hourSelector = selector<number>({
    	key: 'hours',
    	get: ({ get }) => {
    		const minutes = get(minuteState);
    		return minutes / 60;
    	},
    	set: ({ set }, newValue) => {
    		console.log(newValue);
    	}
    });
  2. App.tsx에서 hour가 바뀔 수 있도록 수정하자

    import { useRecoilState, useRecoilValue } from 'recoil';
    import { hourSelector, minuteState } from './atom';
    
    function App() {
    	const [minutes, setMinutes] = useRecoilState(minuteState);
    	const [hours, setHours] = useRecoilState(hourSelector); // selector set 함수 접근을 위해 useRecoilState로 변경
    	const onMinutesChange = (event: React.FormEvent<HTMLInputElement>) => {
    		setMinutes(parseInt(event.currentTarget.value));
    	};
    	const onHoursChange = (event: React.FormEvent<HTMLInputElement>) => {
    		setHours(parseInt(event.currentTarget.value));
    	};
    	return (
    		<div>
    			<input value={minutes} onChange={onMinutesChange} type='number' placeholder='Minutes' />
    			<input value={hours} onChange={onHoursChange} type='number' placeholder='Hours' />
    		</div>
    	);
    }
    export default App;
    💡 setHours 함수는 selector로부터 오는 함수로, selector의 set에 있는 함수를 실행시킨다.

    ⇒ 값들이 콘솔에 잘 들어가고 있다.

  3. atom.tsx 에 hourSelector에서 minuteState를 수정하자

    💡 set(변경하길 원하는 아톰명, 변경 값);

    export const hourSelector = selector<number>({
    	key: 'hours',
    	get: ({ get }) => {
    		const minutes = get(minuteState);
    		return minutes / 60;
    	},
    	set: ({ set }, newValue) => {
    		const minutes = Number(newValue) * 60;
    		set(minuteState, minutes);
    	}
    });

완성!

6.2 Drage and Drop part One


react-beautiful-dnd 라이브러리를 사용해 드래그앤 드롭을 배워보자.

  • 세팅
    • App.tsx 초기화
    • atom.tsx 초기화
  • drag-and-grop context : 기본적으로 드래그 앤 드롭을 가능하게 하고 싶은 앱의 부분
  • droppable : 어떤 것을 드롭할 수 있는 영역
  • draggable : 드래그할 수 있는 영역
  1. react beautiful dnd 라이브러리를 다운로드하자.

    npm i react-beautiful-dnd

    npm i --save-dev @types/react-beautiful-dnd

  2. App.tsx에서 DragDropContext를 만들자

    ⇒ DragDropContext에는 onDragEnd함수, children이 필요하다.

    💡 onDragEnd : 유저가 드래그를 끝낸 시점에 불려지는 함수

    import { DragDropContext } from 'react-beautiful-dnd';
    
    function App() {
    	const onDragEnd = () => {};
    	return (
    		<DragDropContext onDragEnd={onDragEnd}>
    			<div></div>
    		</DragDropContext>
    	);
    }
    export default App;
  3. Droppable을 만들자

    ⇒ Droppable은 droppableId, children가 필요하다

    ⛔ Droppable의 children은 react 요소이면 안되고, **함수**여야 한다!
    import { DragDropContext, Droppable } from 'react-beautiful-dnd';
    
    function App() {
    	const onDragEnd = () => {};
    	return (
    		<DragDropContext onDragEnd={onDragEnd}>
    			<div>
    				<Droppable droppableId='one'>{() => <ul></ul>}</Droppable>
    			</div>
    		</DragDropContext>
    	);
    }
    export default App;
  4. Draggable을 만들자

    ⇒ Draggable은 draggableId, index, children이 필요하다.

    ⇒ Draggable 또한 Droppable과 같이 children에 함수가 들어가야 한다.

    import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
    
    function App() {
    	const onDragEnd = () => {};
    	return (
    		<DragDropContext onDragEnd={onDragEnd}>
    			<div>
    				<Droppable droppableId='one'>
    					{() => (
    						<ul>
    							<Draggable draggableId='first' index={0}>
    								{() => <li>One</li>}
    							</Draggable>
    							<Draggable draggableId='second' index={1}>
    								{() => <li>Two</li>}
    							</Draggable>
    						</ul>
    					)}
    				</Droppable>
    			</div>
    		</DragDropContext>
    	);
    }
    export default App;

완성!

6.3 Drag and Drop part Two


beautiful-dnd가 제공하는 특별한 prop들을 받아보자

  1. Droppable에 prop들을 넣어보자.

    ⇒ magic 이름은 아무상관이 없다. 아무렇게나 해도 됨.

    <Droppable droppableId='one'>
    	{(magic) => (
    		<ul ref={magic.innerRef} {...magic.droppableProps}>
    			<Draggable draggableId='first' index={0}>
    				{() => <li>One</li>}
    			</Draggable>
    			<Draggable draggableId='second' index={1}>
    				{() => <li>Two</li>}
    			</Draggable>
    		</ul>
    	)}
    </Droppable>
  2. Draggable에 prop들을 넣어보자

    💡 dragHandleProps : 사방에서 드래그가 보통 가능한데, 코너 등 특정 부분에서만 드래그가 가능하도록 처리하는 것

    <ul ref={magic.innerRef} {...magic.droppableProps}>
    	<Draggable draggableId='first' index={0}>
    		{(magic) => (
    			<li ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
    				One
    			</li>
    		)}
    	</Draggable>
    	<Draggable draggableId='second' index={1}>
    		{(magic) => (
    			<li ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
    				Two
    			</li>
    		)}
    	</Draggable>
    </ul>

완성!

🪄 또다른 Handle을 만들어 전체 클릭 허용이 아니라 움직일 수 있는 특정한 부분을 만들어 보자
<Draggable draggableId='first' index={0}>
		{(magic) => (
		<li ref={magic.innerRef} {...magic.draggableProps}>
			<span {...magic.dragHandleProps}>🔥</span>
			One
		</li>
	)}
</Draggable>
<Draggable draggableId='second' index={1}>
	{(magic) => (
		<li ref={magic.innerRef} {...magic.draggableProps}>
			<span {...magic.dragHandleProps}>🔥</span>
			Two
		</li>
	)}
</Draggable>

완성!

6.4 Styles and Placeholders


테마 스타일을 변경하자

  1. Theme.ts 에서 색상을 변경하자

    import { DefaultTheme } from 'styled-components';
    
    export const darkTheme: DefaultTheme = {
    	bgColor: '#3F8CF2',
    	boardColor:"#DADFE9",
    	cardColor: "white",
    };
  2. styled.d.ts 에 변경되었음을 알려주자

    // import original module declarations
    import 'styled-components';
    
    // and extend them!
    declare module 'styled-components' {
    	export interface DefaultTheme {
    		bgColor: string;
    		boardColor: string;
    		cardColor: string;
    	}
    }
  3. index.tsx의 GlobalStyle의 body도 변화에 맞게 변경하자

    body{
    		font-family: 'Source Sans Pro', sans-serif;
    		background-color: ${(props) => props.theme.bgColor};
    		color: black;
    	}
  4. App.tsx에 Board, Card의 styled component를 만들어주자

    import styled from 'styled-components';
    
    const Board = styled.div`
    background-color : ${(props) => props.theme.boardColor}}`;
    
    const Card = styled.div`
    background-color : ${(props) => props.theme.cardColor}}`;
  5. Board, Card로 ul, li를 변경하자

    function App() {
    	const onDragEnd = () => {};
    	return (
    		<DragDropContext onDragEnd={onDragEnd}>
    			<div>
    				<Droppable droppableId='one'>
    					{(magic) => (
    						<Board ref={magic.innerRef} {...magic.droppableProps}>
    							<Draggable draggableId='first' index={0}>
    								{(magic) => (
    									<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
    										One
    									</Card>
    								)}
    							</Draggable>
    						</Board>
    					)}
    				</Droppable>
    			</div>
    		</DragDropContext>
    	);
  6. Style 추가

    import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
    import styled from 'styled-components';
    
    const Wrapper = styled.div`
    	display: flex;
    	max-width: 480px;
    	width: 100%;
    	margin: 0 auto;
    	justify-content: center;
    	align-items: center;
    	height: 100vh;
    `;
    
    const Boards = styled.div`
    	display: grid;
    	width: 100%;
    	grid-template-columns: repeat(1, 1fr);
    `;
    
    const Board = styled.div`
    padding: 20px 10px;
    background-color : ${(props) => props.theme.boardColor}}
    padding-top: 30px;
    border-radius: 5px;
    min-height: 200px;
    `;
    const Card = styled.div`
    border-radius: 5px;
    padding: 5px 10px;
    margin-bottom: 5px;
    background-color : ${(props) => props.theme.cardColor}}`;
    
    function App() {
    	const onDragEnd = () => {};
    	return (
    		<DragDropContext onDragEnd={onDragEnd}>
    			<Wrapper>
    				<Boards>
    					<Droppable droppableId='one'>
    						{(magic) => (
    							<Board ref={magic.innerRef} {...magic.droppableProps}>
    								<Draggable draggableId='first' index={0}>
    									{(magic) => (
    										<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
    											One
    										</Card>
    									)}
    								</Draggable>
    							</Board>
    						)}
    					</Droppable>
    				</Boards>
    			</Wrapper>
    		</DragDropContext>
    	);
    }
    export default App;
  7. toDo를 추가해 toDo에 맞게 card가 나오도록 해보자

    const toDos = ['a', 'b', 'c', 'd', 'e', 'f'];
    
    function App() {
    	const onDragEnd = () => {};
    	return (
    		<DragDropContext onDragEnd={onDragEnd}>
    			<Wrapper>
    				<Boards>
    					<Droppable droppableId='one'>
    						{(magic) => (
    							<Board ref={magic.innerRef} {...magic.droppableProps}>
    								{toDos.map((toDo, index) => (
    									<Draggable draggableId={toDo} index={index}>
    										{(<magic) => (
    											<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
    												{toDo}
    											</Card>
    										)}
    									</Draggable>
    								))}
    								{magic.placeholder}
    							</Board>
    						)}
    					</Droppable>
    				</Boards>
    			</Wrapper>
    		</DragDropContext>
    	);
    }

    💡 Board 밑에 placeholder를 붙여주면 사이즈가 변화하지 않는다.

완성!

6.5 Reordering


아이템을 드롭했을 때 재정렬하는 기능을 구현해보자.

  1. toDo State를 위한 atom을 만들자

    import { atom, selector } from 'recoil';
    
    export const toDoState = atom({
    	key: 'toDo',
    	default: ['a', 'b', 'c', 'd', 'e', 'f']
    });
  2. App.tsx에 toDoState 값을 가져오자

    const [toDos, setToDos] = useRecoilState(toDoState);
  3. App.tsx에 onDragEnd 함수를 구현하자 - 1

    onDragEnd : 드래그가 끝났을 때 실행되는 함수

    const onDragEnd = () => {
    	console.log('draggin finished');
    };

<aside>

💡 onDragEnd는 드래그가 끝났을 때 많은 것을 알려준다!

const onDragEnd = (args: any) => {
	console.log(args);
};

⇒ 무엇을 드래그 한 것인지, 드래그해서 어디로 가는지 등…

4. App.tsx에 onDragEnd 함수를 구현하자 - 2
  • 원리 : source의 인덱스에 해당하는 값을 배열에서 삭제하고, destination의 인덱스 위치에 추가한다.
  • splice 사용! : splice(시작 인덱스, 없앨 개수, 대체할 문자);

6.6 Reordering part Two


  1. App.tsx의 setToDos() 함수를 구현하자

    const onDragEnd = ({ draggableId, destination, source }: DropResult) => {
    	if (!destination) return;
    	setToDos((oldToDos) => {
    		const copyToDos = [...oldToDos];
    		// 1) Delete item on source.index
    		copyToDos.splice(source.index, 1);
    		// 2) Put back the item on the destination.index
    		copyToDos.splice(destination?.index, 0, draggableId);
    		return copyToDos;
    	});
    };
  2. Draggable의 key를 toDo로 변경하자

    <Board ref={magic.innerRef} {...magic.droppableProps}>
    	{toDos.map((toDo, index) => (
    		<Draggable key={toDo} draggableId={toDo} index={index}>
    			{(magic) => (
    				<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
    					{toDo}
    				</Card>
    			)}
    		</Draggable>
    	))}
    	{magic.placeholder}
    </Board>

    💡 Draggable에서 key와 draggableId는 같아야 한다!

완성!

⛔ But, 매번 렌더링되어 약간의 간극이 발생!

→ 다음 시간에 해결해보자!

6.7 Performance


움직일 때 글자가 가끔 떨리는 문제를 해결해보자

  1. 코드를 정리해보자
    a. Components 폴더 생성 후, 안에 DraggableCard.tsx 생성

    ```jsx
    import { Draggable } from 'react-beautiful-dnd';
    import styled from 'styled-components';
    
    const Card = styled.div`
    border-radius: 5px;
    padding: 5px 10px;
    margin-bottom: 5px;
    background-color : ${(props) => props.theme.cardColor}}`;
    
    interface IDraggableCardProps {
    	toDo: string;
    	index: number;
    }
    
    function DraggableCard({ toDo, index }: IDraggableCardProps) {
    	return (
    		<Draggable key={toDo} draggableId={toDo} index={index}>
    			{(magic) => (
    				<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
    					{toDo}
    				</Card>
    			)}
    		</Draggable>
    	);
    }
    
    export default DraggableCard;
    ```

    b. App.tsx에 DraggableCard 추가

    ```jsx
    return (
    	<DragDropContext onDragEnd={onDragEnd}>
    		<Wrapper>
    			<Boards>
    				<Droppable droppableId='one'>
    					{(magic) => (
    						<Board ref={magic.innerRef} {...magic.droppableProps}>
    							{toDos.map((toDo, index) => (
    								<DraggableCard key={toDo} index={index} toDo={toDo} />
    							))}
    							{magic.placeholder}
    						</Board>
    					)}
    				</Droppable>
    			</Boards>
    		</Wrapper>
    	</DragDropContext>
    );
    ```
  2. react memo를 사용해 계속 렌더링되는 것을 방지한다.

    • 리액트는 State가 변화하면 모든게 새로고침된다. → 계속 렌더링됨.

    💡 react memo는 react.js한테 prop이 바뀌지 않는다면 컴포넌트를 렌더링 하지 말라고 한다!

    import React from 'react';
    
    export default React.memo(DraggableCard);
⛔ 리렌더링 시에 스타일이 적용안되는 오류 발생..

6.8 Multi Boards


여러 개의 보드를 만들어보자

  1. state를 object로 만들어주자

    import { atom, selector } from 'recoil';
    
    export const toDoState = atom({
    	key: 'toDo',
    	default: {
    		to_do: ['a', 'b'],
    		doing: ['c', 'd'],
    		done: ['e', 'f']
    	}
    });
  2. Components 폴더 안에 Board.tsx 파일을 생성하자

    import { Droppable } from 'react-beautiful-dnd';
    import styled from 'styled-components';
    import DraggableCard from './DraggableCard';
    
    const Wrapper = styled.div`
    padding: 20px 10px;
    background-color : ${(props) => props.theme.boardColor}};
    border-radius: 5px;
    min-height: 200px;
    `;
    
    interface IBoardProps {
    	toDos: string[];
    	boardId: string;
    }
    
    function Board({ toDos, boardId }: IBoardProps) {
    	return (
    		<Droppable droppableId={boardId}>
    			{(magic) => (
    				<Wrapper ref={magic.innerRef} {...magic.droppableProps}>
    					{toDos.map((toDo, index) => (
    						<DraggableCard key={toDo} index={index} toDo={toDo} />
    					))}
    					{magic.placeholder}
    				</Wrapper>
    			)}
    		</Droppable>
    	);
    }
    
    export default Board;
  3. Atoms.tsx 변경

    import { DragDropContext, DropResult } from 'react-beautiful-dnd';
    import { useRecoilState } from 'recoil';
    import styled from 'styled-components';
    import { toDoState } from './atom';
    import DraggableCard from './Components/DraggableCard';
    import Board from './Components/Board';
    
    const Wrapper = styled.div`
    	display: flex;
    	width: 100%;
    	max-width: 680px;
    	margin: 0 auto;
    	justify-content: center;
    	align-items: center;
    	height: 100vh;
    `;
    
    const Boards = styled.div`
    	display: grid;
    	width: 100%;
    	gap: 10px;
    	grid-template-columns: repeat(3, 1fr);
    `;
    
    function App() {
    	const [toDos, setToDos] = useRecoilState(toDoState);
    	const onDragEnd = ({ draggableId, destination, source }: DropResult) => {
    		if (!destination) return;
    		// setToDos((oldToDos) => {
    		// 	const copyToDos = [...oldToDos];
    		// 	// 1) Delete item on source.index
    		// 	copyToDos.splice(source.index, 1);
    		// 	// 2) Put back the item on the destination.index
    		// 	copyToDos.splice(destination?.index, 0, draggableId);
    		// 	return copyToDos;
    		// });
    	};
    	return (
    		<DragDropContext onDragEnd={onDragEnd}>
    			<Wrapper>
    				<Boards>
    					{Object.keys(toDos).map((boardId) => (
    						<Board boardId={boardId} key={boardId} toDos={toDos[boardId]} />
    					))}
    				</Boards>
    			</Wrapper>
    		</DragDropContext>
    	);
    }
    export default App;
  4. atom.tsx 에 인터페이스를 추가하자

    import { atom, selector } from 'recoil';
    
    interface IToDoState {
    	[key: string]: string[];
    }
    
    export const toDoState = atom<IToDoState>({
    	key: 'toDo',
    	default: {
    		to_do: ['a', 'b'],
    		doing: ['c', 'd'],
    		done: ['e', 'f']
    	}
    });

완성!

6.9 Same Board Movement


Board 안에서 카드 재배열을 다시 구현하자!

  • 세팅
    • Board.tsx
      import { Droppable } from 'react-beautiful-dnd';
      import styled from 'styled-components';
      import DraggableCard from './DraggableCard';
      
      const Wrapper = styled.div`
         width: 300px;
         padding: 20px 10px;
         padding-top: 10px;
         background-color : ${(props) => props.theme.boardColor}};
         border-radius: 5px;
         min-height: 300px;
      `;
      
      const Title = styled.h2`
      	text-align: center;
      	font-weight: 600;
      	maring-bottom: 10px;
      	font-size: 18px;
      `;
      
      interface IBoardProps {
      	toDos: string[];
      	boardId: string;
      }
      
      function Board({ toDos, boardId }: IBoardProps) {
      	return (
      		<Wrapper>
      			<Title>{boardId}</Title>
      			<Droppable droppableId={boardId}>
      				{(magic) => (
      					<div ref={magic.innerRef} {...magic.droppableProps}>
      						{toDos.map((toDo, index) => (
      							<DraggableCard key={toDo} index={index} toDo={toDo} />
      						))}
      						{magic.placeholder}
      					</div>
      				)}
      			</Droppable>
      		</Wrapper>
      	);
      }
      
      export default Board;
    • App.tsx
      import { DragDropContext, DropResult } from 'react-beautiful-dnd';
      import { useRecoilState } from 'recoil';
      import styled from 'styled-components';
      import { toDoState } from './atom';
      import DraggableCard from './Components/DraggableCard';
      import Board from './Components/Board';
      
      const Wrapper = styled.div`
      	display: flex;
      	width: 100vw;
      	max-width: 680px;
      	margin: 0 auto;
      	justify-content: center;
      	align-items: center;
      	height: 100vh;
      `;
      
      const Boards = styled.div`
      	display: flex;
      	justify-content: center;
      	align-items: center;
      	width: 100%;
      	gap: 10px;
      `;
      
      function App() {
      	const [toDos, setToDos] = useRecoilState(toDoState);
      	const onDragEnd = ({ draggableId, destination, source }: DropResult) => {
      		if (!destination) return;
      		// setToDos((oldToDos) => {
      		// 	const copyToDos = [...oldToDos];
      		// 	// 1) Delete item on source.index
      		// 	copyToDos.splice(source.index, 1);
      		// 	// 2) Put back the item on the destination.index
      		// 	copyToDos.splice(destination?.index, 0, draggableId);
      		// 	return copyToDos;
      		// });
      	};
      	return (
      		<DragDropContext onDragEnd={onDragEnd}>
      			<Wrapper>
      				<Boards>
      					{Object.keys(toDos).map((boardId) => (
      						<Board boardId={boardId} key={boardId} toDos={toDos[boardId]} />
      					))}
      				</Boards>
      			</Wrapper>
      		</DragDropContext>
      	);
      }
      export default App;
    • atom.tsx
      import { atom, selector } from 'recoil';
      
      interface IToDoState {
      	[key: string]: string[];
      }
      
      export const toDoState = atom<IToDoState>({
      	key: 'toDo',
      	default: {
      		'To Do': ['a', 'b'],
      		Doing: ['c', 'd'],
      		Done: ['e', 'f']
      	}
      });
  1. App.tsx의 onDragEnd를 수정하자 - 1

    const onDragEnd = (info: DropResult) => {
    	console.log(info);
    };

  2. App.tsx의 onDragEnd를 수정하자 - 2

    • 원리 : 수정이 일어난 보드만 복사 → 복사본을 기존 애들 옆에 붙여준다.
    const onDragEnd = (info: DropResult) => {
    		console.log(info);
    		const { destination, draggableId, source } = info;
    		if (destination?.droppableId === source.droppableId) {
    			setToDos((oldToDos) => {
    				const boardCopy = [...oldToDos[source.droppableId]];
    				boardCopy.splice(source.index, 1);
    				boardCopy.splice(destination?.index, 0, draggableId);
    				return {
    					...oldToDos,
    					[source.droppableId]: boardCopy
    				};
    			});
    		}
    	};

완성!

6.10 Cross Board Movement


보드를 넘나드는 이동을 만들어보자

  1. App.tsx에 구현하자

    • 원리 : source array, desitnation array 둘 다 복사
    if (destination?.droppableId !== source.droppableId) {
    	// cross board movement
    	setToDos((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
    		};
    	});
    }

완성!

⛔ But, 움직일 때마다 맨 위로 가야지만 이동이 끝나는 문제 발생

→ 다음 시간에 해결해보자!

6.11 Droppable Snapshot


보드를 떠날 때 색상을 바꿔야할 타이밍과 목적지 보드에 도착했을 때 색상을 바꿔야 할 타이밍을 정해보자

  1. Board.tsx에 Area 컴포넌트를 추가하자

    const Area = styled.div`
    	background-color: blue;
    `;
    
    function Board({ toDos, boardId }: IBoardProps) {
    	return (
    		<Wrapper>
    			<Title>{boardId}</Title>
    			<Droppable droppableId={boardId}>
    				{(magic) => (
    					<Area ref={magic.innerRef} {...magic.droppableProps}>
    						{toDos.map((toDo, index) => (
    							<DraggableCard key={toDo} index={index} toDo={toDo} />
    						))}
    						{magic.placeholder}
    					</Area>
    				)}
    			</Droppable>
    		</Wrapper>
    	);
    }
  2. Area 영역을 늘려 아무데나 이동할 수 있게 해보자.

    const Wrapper = styled.div`
       width: 300px;
       padding: 20px 10px;
       padding-top: 10px;
       background-color : ${(props) => props.theme.boardColor}};
       border-radius: 5px;
       min-height: 300px;
    	display: flex;
    	flex-direction: column;
    `;
    
    const Area = styled.div`
    	background-color: blue;
    	flex-grow: 1;
    `;

    💡 flex-grow : 자동으로 플렉스박스 아이템의 크기를 늘리게 해준다.

  3. snapshot을 사용해 이동할 보드의 Area의 색상을 변경해주자

    💡 snapshot : isDraggingOver에 대한 boolean 값을 넘겨 board로 들어왔는지 알려주고,

                   draggingFromThisWith로 DraggableID를 넘겨 드래그를 시작한 아이디를 알려줌.

    ⇒ snapshot의 이름은 info로 대체했다.

    const Area = styled.div<{ isDraggingOver: boolean }>`
    	background-color: ${(props) => (props.isDraggingOver ? 'pink' : 'blue')};
    	flex-grow: 1;
    `;
    
    function Board({ toDos, boardId }: IBoardProps) {
    	return (
    		<Wrapper>
    			<Title>{boardId}</Title>
    			<Droppable droppableId={boardId}>
    				{(magic, info) => (
    					<Area isDraggingOver={info.isDraggingOver} ref={magic.innerRef} {...magic.droppableProps}>
    						{toDos.map((toDo, index) => (
    							<DraggableCard key={toDo} index={index} toDo={toDo} />
    						))}
    						{magic.placeholder}
    					</Area>
    				)}
    			</Droppable>
    		</Wrapper>
    	);
    }
  4. snapshot을 사용해 떠난 보드의 Area 색상을 변경해주자

    interface IAreaProps {
    	isDraggingOver: boolean;
    	isDraggingFromThis: boolean;
    }
    
    const Area = styled.div<IAreaProps>`
    	background-color: ${(props) => (props.isDraggingOver ? 'pink' : props.isDraggingFromThis ? 'red' : 'blue')};
    	flex-grow: 1;
    `;
    
    <Droppable droppableId={boardId}>
    	{(magic, info) => (
    		<Area isDraggingOver={info.isDraggingOver} isDraggingFromThis={Boolean(info.draggingFromThisWith)} ref={magic.innerRef} {...magic.droppableProps}>
    			{toDos.map((toDo, index) => (
    				<DraggableCard key={toDo} index={index} toDo={toDo} />
    			))}
    			{magic.placeholder}
    		</Area>
    	)}
    </Droppable>
  5. Area에 transition을 넣어주자

    const Area = styled.div<IAreaProps>`
    	background-color: ${(props) => (props.isDraggingOver ? 'pink' : props.isDraggingFromThis ? 'red' : 'blue')};
    	flex-grow: 1;
    	transition: background-color 0.3s ease-in-out;
    `;

완성!

6.12 Final Styles


드래그하고 있는 카드의 스타일을 변경해보자

  1. DraggableCard.tsx에서 snapshot을 사용해 스타일을 변경해보자

    const Card = styled.div<{ isDragging: boolean }>`
    	border-radius: 5px;
    	padding: 10px 10px;
    	margin-bottom: 5px;
    	background-color: ${(props) => (props.isDragging ? '#74b9ff' : props.theme.cardColor)};
    	box-shadow: ${(props) => (props.isDragging ? '0px 2px 5px rgba(0,0,0,0.5)' : 'none')};
    `;
    
    function DraggableCard({ toDo, index }: IDraggableCardProps) {
    	return (
    		<Draggable key={toDo} draggableId={toDo} index={index}>
    			{(magic, snapshot) => (
    				<Card isDragging={snapshot.isDragging} ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
    					{toDo}
    				</Card>
    			)}
    		</Draggable>
    	);
    }
  2. Board.tsx의 스타일도 변경해주자

    const Wrapper = styled.div`
       width: 300px;
       padding-top: 10px;
       background-color : ${(props) => props.theme.boardColor}};
       border-radius: 5px;
       min-height: 300px;
    	display: flex;
    	flex-direction: column;
    `;
    
    const Area = styled.div<IAreaProps>`
    	background-color: ${(props) => (props.isDraggingOver ? '#dfe6e9' : props.isDraggingFromThis ? '#b2bec3' : 'transparent')};
    	flex-grow: 1;
    	transition: background-color 0.3s ease-in-out;
    	padding: 20px;
    `;

완성!

6.13 Refs


ref 사용법을 알아보자

💡 reference : react 코드를 이용해 HTML 요소를 요소를 지정하고, 가져올 수 있는 방법.

  1. useRef 사용

    import { useRef } from 'react';
    
    function Board({ toDos, boardId }: IBoardProps) {
    	const inputRef = useRef<HTMLInputElement>(null);
    	const onClick = () => {
    		inputRef.current?.focus();
    		setTimeout(() => {
    			inputRef.current?.blur();
    		}, 5000);
    	};
    	return (
    		<Wrapper>
    			<Title>{boardId}</Title>
    			<input ref={inputRef} placeholder='grab me' />
    			<button onClick={onClick}>Click me</button>
    			<Droppable droppableId={boardId}>
    				{(magic, info) => (
    					<Area isDraggingOver={info.isDraggingOver} isDraggingFromThis={Boolean(info.draggingFromThisWith)} ref={magic.innerRef} {...magic.droppableProps}>
    						{toDos.map((toDo, index) => (
    							<DraggableCard key={toDo} index={index} toDo={toDo} />
    						))}
    						{magic.placeholder}
    					</Area>
    				)}
    			</Droppable>
    		</Wrapper>
    	);
    }

완성!

6.14 Task Objects


react-hook-form의 useForm를 사용해 폼을 만들어보자

  1. react-hook-form을 설치하자

    npm i react-hook-form

  2. useForm을 불러오고 인터페이스를 생성하자

    import { useForm } from 'react-hook-form';
    
    interface IForm {
    	toDo: string;
    }
  3. form을 구현하자

    const Form = styled.form`
    	width: 100%;
    	input {
    		width: 100%;
    	}
    `;
    
    function Board({ toDos, boardId }: IBoardProps) {
    	const { register, setValue, handleSubmit } = useForm<IForm>();
    
    	return (
    		<Wrapper>
    			<Title>{boardId}</Title>
    			<Form>
    				<input {...register('toDo', { required: true })} placeholder={`Add task on ${boardId}`} />
    			</Form>
    			<Droppable droppableId={boardId}>
    				{(magic, info) => (
    					<Area isDraggingOver={info.isDraggingOver} isDraggingFromThis={Boolean(info.draggingFromThisWith)} ref={magic.innerRef} {...magic.droppableProps}>
    						{toDos.map((toDo, index) => (
    							<DraggableCard key={toDo} index={index} toDo={toDo} />
    						))}
    						{magic.placeholder}
    					</Area>
    				)}
    			</Droppable>
    		</Wrapper>
    	);
    }
  4. onValid를 설정해 IForm 타입의 데이터를 받자

    const onValid = (data: IForm) => {
    		
    	};
    	return (
    		<Wrapper>
    			<Title>{boardId}</Title>
    			<Form onSubmit={handleSubmit(onValid)}>
  5. atoms.tsx에서 state를 변경하자

    import { atom, selector } from 'recoil';
    
    export interface IToDo {
    	id: number;
    	text: string;
    }
    
    interface IToDoState {
    	[key: string]: IToDo[];
    }
    
    export const toDoState = atom<IToDoState>({
    	key: 'toDo',
    	default: {
    		'To Do': [],
    		Doing: [],
    		Done: []
    	}
    });
  6. prop들을 모두 업데이트하자

    • Board.tsx
      interface IBoardProps {
      	toDos: ITodo[];
      	boardId: string;
      }
      
      function Board({ toDos, boardId }: IBoardProps) {
      	const { register, setValue, handleSubmit } = useForm<IForm>();
      	const onValid = (data: IForm) => {};
      	return (
      		<Wrapper>
      			<Title>{boardId}</Title>
      			<Form onSubmit={handleSubmit(onValid)}>
      				<input {...register('toDo', { required: true })} placeholder={`Add task on ${boardId}`} />
      			</Form>
      			<Droppable droppableId={boardId}>
      				{(magic, info) => (
      					<Area isDraggingOver={info.isDraggingOver} isDraggingFromThis={Boolean(info.draggingFromThisWith)} ref={magic.innerRef} {...magic.droppableProps}>
      						{toDos.map((toDo, index) => (
      							<DraggableCard key={toDo.id} index={index} toDoId={toDo.id} toDoText={toDo.text} />
      						))}
      						{magic.placeholder}
      					</Area>
      				)}
      			</Droppable>
      		</Wrapper>
      	);
      }
    • DraggableCard.tsx
      interface IDraggableCardProps {
      	toDoId: number;
      	toDoText: string;
      	index: number;
      }
      
      function DraggableCard({ toDoId, toDoText, index }: IDraggableCardProps) {
      	return (
      		<Draggable key={toDoId + ''} draggableId={toDoId + ''} index={index}>
      			{(magic, snapshot) => (
      				<Card isDragging={snapshot.isDragging} ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
      					{toDoText}
      				</Card>
      			)}
      		</Draggable>
      	);
      }

6.15 Creating Tasks


  1. App.tsx에서 onDragEnd 함수를 obj로 변환해 옮겨주자

    const onDragEnd = (info: DropResult) => {
    		console.log(info);
    		const { destination, draggableId, source } = info;
    		if (!destination) return;
    		if (destination?.droppableId === source.droppableId) {
    			// same board movement
    			setToDos((oldToDos) => {
    				const boardCopy = [...oldToDos[source.droppableId]];
    				const taskObj = boardCopy[source.index];
    				boardCopy.splice(source.index, 1);
    				boardCopy.splice(destination?.index, 0, taskObj);
    				return {
    					...oldToDos,
    					[source.droppableId]: boardCopy
    				};
    			});
    		}
    		if (destination?.droppableId !== source.droppableId) {
    			// cross board movement
    			setToDos((allBoards) => {
    				const sourceBoard = [...allBoards[source.droppableId]];
    				const taskObj = sourceBoard[source.index];
    				const destinationBoard = [...allBoards[destination.droppableId]];
    				sourceBoard.splice(source.index, 1);
    				destinationBoard.splice(destination?.index, 0, taskObj);
    				return {
    					...allBoards,
    					[source.droppableId]: sourceBoard,
    					[destination.droppableId]: destinationBoard
    				};
    			});
    		}
    	};
  2. Board.tsx에서 Task를 추가해보자

    const onValid = ({ toDo }: IForm) => {
    		const newToDo = {
    			id: Date.now(),
    			text: toDo
    		};
    		setToDos((allBoards) => {
    			return {
    				...allBoards,
    				[boardId]: [...allBoards[boardId], newToDo]
    			};
    		});
    		setValue('toDo', '');
    	};

완성!

6.16 Code Challenge


💪 Challenge

  • input 스타일 변경하기
  • 새로고침해도 task가 안 사라지게 모든 task state를 local storage에 저장하기
  • task를 삭제할 수 있게 하기 → 쓰레기통 구현 or 삭제 버튼
  • Board 자체의 순서를 바꿀 수 있게 하기
  • Board를 추가할 수 있게 하기
profile
프론트엔드 / 컴퓨터공학과 4학년

0개의 댓글