컴포넌트 생성
빨간색 네모가 BoardList 컴포넌트, 파란색 네모가 ListsContainer 컴포넌트이다.
function App() {
const [activeBoardId, setActiveBoardId] = useState('board-0');
const boards = useTypedSelector(state => state.boards.boardArray);
const getActiveBoard = boards.filter(board => board.boardId === activeBoardId)[0];
const lists = getActiveBoard.lists;
return (
<div className={appContainer}>
<BoardList
activeBoardId={activeBoardId}
setActiveBoardId={setActiveBoardId}>
</BoardList>
<div className={board}>
<ListsContainer lists={lists} boardId={getActiveBoard.boardId} />
</div>
...
</div>
)
}
Board List 생성
- 활성화된 게시판 id(activeBoardId)와 활성화된 게시판 id를 설정할 수 있는 함수(setIsFormOpen)를 인수로 받는다.
- activeBoardId를 이용하여 활성화된 게시판 버튼과 아닌 버튼에 각각 스타일을 적용한다.
- isFormOpen은 게시판을 추가할 수 있는 입력란이 활성화되어 있는지를 나타낸다.
- false면 플러스 모양 아이콘을, true면 입력란(SideForm 컴포넌트)을 보여준다.
...
type TBoardListProps = {
activeBoardId: string;
setActiveBoardId: React.Dispatch<React.SetStateAction<string>>;
}
const BoardList: FC<TBoardListProps> = ({
activeBoardId,
setActiveBoardId
}) => {
const { boardArray } = useTypedSelector(state => state.boards);
const [isFormOpen, setIsFormOpen] = useState(false);
return (
<div className={container}>
<div className={title}>
게시판:
</div>
{
boardArray.map((board, index) => (
<div key={board.boardId}
onClick={() => setActiveBoardId(board.boardId)}
className={
clsx(
{
[boardItemActive]:
boardArray.findIndex(b => b.boardId === activeBoardId) === index
},
{
[boardItem]:
boardArray.findIndex(b => b.boardId === activeBoardId) !== index
})}
>
<div>
{board.boardName}
</div>
</div>
))
}
<div className={addSection}>
{
isFormOpen ?
<SideForm setIsFormOpen={setIsFormOpen} /> :
<FiPlusCircle className={addButton} onClick={() => setIsFormOpen(!isFormOpen)} />
}
</div>
</div>
)
}
SideForm 생성
- 입력란을 벗어나면 다시 플러스 아이콘이 보여야 하기 때문에 인수로 setIsFormOpen을 받는다.
- 입력란을 벗어나는 이벤트는 blur다. blur가 발생하면 상위 컴포넌트인 BoardList 컴포넌트의 isFormOpen을 false로 바꿔서 플러스 아이콘이 보이도록 한다.
- 이벤트 발생 순서는 mouseDown -> blur -> mouseUp -> click 순이기 때문에 onClick으로 작성하면 blur 이벤트가 먼저 발생하여 새로운 게시판이 생성되지 않으므로 onMouseDown을 사용한다.
type TSideFormProps = {
setIsFormOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const SideForm: FC<TSideFormProps> = ({
setIsFormOpen
}) => {
const [inputText, setInputText] = useState('');
const dispatch = useTypedDispatch();
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputText(e.target.value);
}
const handleOnBlur = () => {
setIsFormOpen(false);
}
const handleClick = () => {
...
}
return (
<div className={sideForm}>
<input
autoFocus
className={input}
type="text"
placeholder='새로운 게시판 등록하기'
value={inputText}
onChange={handleChange}
onBlur={handleOnBlur}
/>
<FiCheck className={icon} onMouseDown={handleClick} />
</div>
)
}
- 게시판 이름을 입력하고 체크 버튼을 누르면 새로운 게시판이 생성되므로 게시판 목록과 로그 목록에 상태 변화가 발생한다.
- 게시판 목록의 상태와 로그 목록을 변화시키기 위해서 boardSlice의 reducer에 게시판을 추가하는 함수와 loggerSlice의 reducer에 로그를 추가하는 함수를 정의한다.
- immer이라는 라이브러리를 사용하기 때문에 불변성을 신경 쓰지 않아도 된다.
const boardsSlice = createSlice({
name: 'boards',
initialState,
reducers: {
addBoard: (state, { payload }: PayloadAction<TAddBoardAction>) => {
state.boardArray.push(payload.board);
}
}
})
const loggerSlice = createSlice({
name: 'logger',
initialState,
reducers: {
addLog: (state, { payload }: PayloadAction<ILogItem>) => {
state.logArray.push(payload);
}
}
})
const handleClick = () => {
if (inputText) {
dispatch(
addBoard({
board: {
boardId: uuidv4(),
boardName: inputText,
lists: []
}
})
);
dispatch(
addLog({
logId: uuidv4(),
logMessage: `게시판 등록: ${inputText}`,
logAuthor: 'User',
logTimestamp: String(Date.now())
})
);
}
}
DropDownForm 생성
- 새로운 일 등록 버튼이나 새로운 리스트 등록 버튼을 클릭했을 때 나오는 입력창 컴포넌트이다.
- 인수로 게시판 id와 해당 리스트의 id, 추가하기 버튼을 클릭했을 때 입력창을 닫기 위한 setIsFormOpen 상태 설정함수를 받는다. 그리고 list라는 인자를 받는데, list 존재 여부에 따라 task를 추가하는 것인지, 리스트를 추가하는 것인지 결정한다.
type TDropDownFormProps = {
boardId: string;
listId: string;
setIsFormOpen: React.Dispatch<React.SetStateAction<boolean>>
list?: boolean;
}
const DropDownForm: FC<TDropDownFormProps> = ({
boardId,
list,
listId,
setIsFormOpen
}) => {
const dispatch = useTypedDispatch();
const [text, setText] = useState('');
const formPlaceholder = list ? '리스트에 제목을 입력하세요.' : '일의 제목을 입력하세요.'
const buttonTitle = list ? '리스트 추가하기' : '일 추가하기';
const handleTextChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setText(e.target.value)
}
const handleButtonClick = () => {
if (text) {
if (list) {
// list 추가 코드
// 로그 추가 코드
} else {
// task 추가 코드
// 로그 추가 코드
}
}
}
return (
<div className={list ? listForm : taskForm}>
<textarea
className={input}
value={text}
onChange={handleTextChange}
autoFocus
placeholder={formPlaceholder}
onBlur={() => setIsFormOpen(false)}
/>
<div className={buttons}>
<button
className={button}
onMouseDown={handleButtonClick}
>
{buttonTitle}
</button>
<FiX className={close} />
</div>
</div>
)
}
- 새로운 리스트나 새로운 task를 생성했을 경우 컴포넌트가 리렌더링 되어야 한다.
- 새로운 리스트를 추가하는 함수 addList와 새로운 task를 추가하는 함수 addTask를 boardSlice에 아래와 같이 작성한다.
- 입력란에 텍스트를 입력하고 추가하기 버튼을 클릭하면 addList 또는 addTask와 로그를 추가하는 함수인 addLog가 실행되어야 한다.
const boardsSlice = createSlice({
name: 'boards',
initialState,
reducers: {
...
addList: (state, { payload }: PayloadAction<TAddListAction>) => {
state.boardArray.map(board =>
board.boardId === payload.boardId
? { ...board, lists: board.lists.push(payload.list) }
: board
)
},
addTask: (state, { payload }: PayloadAction<TAddTaskAction>) => {
state.boardArray.map(board =>
board.boardId === payload.boardId
? {
...board,
lists: board.lists.map(list =>
list.listId === payload.listId
? { ...list, tasks: list.tasks.push(payload.task) }
: list
)
}
: board
)
}
}
})
const handleButtonClick = () => {
if (text) {
if (list) {
dispatch(
addList({
boardId,
list: { listId: v4(), listName: text, tasks: [] }
})
)
dispatch(
addLog({
logId: v4(),
logMessage: `리스트 생성하기: ${text}`,
logAuthor: 'User',
logTimestamp: String(Date.now())
})
)
} else {
dispatch(
addTask({
boardId: boardId,
listId: listId,
task: {
taskId: v4(),
taskName: text,
taskDescription: '',
taskOwner: 'User'
}
})
)
dispatch(
addLog({
logId: v4(),
logMessage: `일 생성하기: ${text}`,
logAuthor: 'User',
logTimestamp: String(Date.now())
})
)
}
}
}
Action Button 생성
- ActionButton도 list 인자 존재 여부에 따라 리스트 생성의 버튼인지, task 생성의 버튼인지가 결정된다.
- 이 버튼이 클릭되면 입력창(DropDownForm)이 나타나야 한다.
- DropDownForm에서 리스트 또는 task를 추가하기 때문에 boardId 인자를 전달해줘야 한다.
type TActionButtonProps = {
boardId: string;
listId: string;
list?: boolean
}
const ActionButton: FC<TActionButtonProps> = ({
boardId,
listId,
list
}) => {
const [isFormOpen, setIsFormOpen] = useState(false);
const buttonText = list ? '새로운 리스트 등록' : '새로운 일 등록';
return isFormOpen ? (
<DropDownForm
setIsFormOpen={setIsFormOpen}
list={list ? true : false}
boardId={boardId}
listId={listId}
/>
)
:
(
<div
className={list ? listButton : taskButton}
onClick={() => setIsFormOpen(true)}
>
<IoIosAdd />
<p>{buttonText}</p>
</div>
)
}
List Container 생성
- 현재 활성화되어 있는 게시판 id와 해당 게시판의 리스트 목록을 인수로 받는다.
- 각 리스트를 리스트 컴포넌트로 전달해 주고, 활성화되어 있는 게시판 id도 함께 전달한다.
- 게시판 id도 전달하는 이유는 리스트 컴포넌트에서 리스트를 삭제할 수 있게 하기 위함이다.
type TListsContainerProps = {
boardId: string;
lists: IList[];
}
const ListsContainer: FC<TListsContainerProps> = ({
lists,
boardId
}) => {
return (
<div className={listsContainer}>
{
lists.map(list => (
<List
key={list.listId}
list={list}
boardId={boardId}
/>
))
}
<ActionButton
boardId={boardId}
listId={''}
list
/>
</div>
)
}
List 생성
- 해당 리스트의 tasks 배열을 순회하며 Task 컴포넌트들을 생성한다.
- 리스트 제목 옆에 있는 (-) 버튼을 클릭하면 해당 리스트가 삭제되어야 한다.
- task를 클릭하면 상세 정보를 표시해 주는 모달 창이 띄워져야 한다.
type TListProps = {
boardId: string;
list: IList;
}
const List: FC<TListProps> = ({
list,
boardId
}) => {
const dispatch = useTypedDispatch();
const handleListDelete = (listId: string) => {
// 클릭된 리스트 삭제 코드
// 로그 생성 코드
}
const handleTaskChange = (
boardId: string,
listId: string,
taskId: string,
task: ITask
) => {
// 모달에 표시할 데이터 저장 코드
// 모달 활성화 코드
}
return (
<div className={listWrapper}>
<div className={header}>
<div className={name}>{list.listName}</div>
<GrSubtract
className={deleteButton}
onClick={() => handleListDelete(list.listId)}
/>
</div>
{
list.tasks.map((task, index) => (
<div
onClick={() => handleTaskChange(boardId, list.listId, task.taskId, task)}
key={task.taskId}
>
<Task
taskName={task.taskName}
taskDescription={task.taskDescription}
boardId={boardId}
id={task.taskId}
index={index}
/>
</div>
))
}
<ActionButton
boardId={boardId}
listId={list.listId}
/>
</div>
)
}
- 리스트를 삭제해 주는 함수 deleteList와 task 클릭 시 모달에 표시할 task를 설정하는 setModalData 함수, 모달 활성화 여부를 설정해 주는 setModalActive 함수를 boardSlice에 정의한다.
const boardsSlice = createSlice({
name: 'boards',
initialState,
reducers: {
...
deleteList: (state, { payload }: PayloadAction<TDeleteListAction>) => {
state.boardArray = state.boardArray.map(
board => board.boardId === payload.boardId
?
{
...board,
lists: board.lists.filter(
list => list.listId !== payload.listId
)
}
:
board
)
},
setModalActive: (state, { payload }: PayloadAction<boolean>) => {
state.modalActive = payload
}
}
})
const handleListDelete = (listId: string) => {
dispatch(deleteList({ boardId, listId }));
dispatch(addLog({
logId: v4(),
logMessage: `리스트 삭제하기: ${list.listName}`,
logAuthor: 'User',
logTimestamp: String(Date.now())
}))
}
const handleTaskChange = (
boardId: string,
listId: string,
taskId: string,
task: ITask
) => {
dispatch(setModalData({ boardId, listId, task }));
dispatch(setModalActive(true));
}
Task 생성
- task의 제목과 설명을 보여준다.
- 인자 중 id는 해당 task의 id이고, index는 추후 drag and drop 기능을 구현할 때 사용된다.
type TTaskProps = {
index: number;
id: string;
boardId: string;
taskName: string;
taskDescription: string;
}
const Task: FC<TTaskProps> = ({
index,
id,
boardId,
taskName,
taskDescription
}) => {
return (
<div className={container}>
<div className={title}>{taskName}</div>
<div className={description}>{taskDescription}</div>
</div>
)
}
배운 점
- 생성한 Slice의 reducer에 상태를 변경하는 함수를 작성한다.
- 위 함수를 export 하면 다른 컴포넌트에서 import 하여 dispatch로 해당 함수를 사용할 수 있다.
- 이벤트 발생 순서는 mouseDown -> blur -> mouseUp -> click 순이다.
- immer이라는 라이브러리를 사용하기 때문에 불변성을 신경 쓰지 않아도 되기 때문에 Slice의 reducer에 정의한 함수에서는 push를 사용해도 된다.
'데브코스' 카테고리의 다른 글
[13주차 - DAY1] 게시판 만들기(4) & 배포 (0) | 2024.05.20 |
---|---|
[12주차 - DAY5] 게시판 만들기(3) (0) | 2024.05.17 |
[12주차 주간 발표] 인덱스 (0) | 2024.05.15 |
[12주차 - DAY2] 게시판 만들기(1) (0) | 2024.05.14 |
[12주차 - DAY1] TodoList 만들기(2) (0) | 2024.05.13 |