
이번주는 12주차에 만들었던 folder페이지에서 폴더에 대한 CRUD를 구현하는 것이 목표이다. 폴더를 생성, 조회, 이름 수정, 삭제를 api호출을 통해 구현하고, 사용자 화면에 바로 반영하는것 까지가 완성이다.
폴더 조회는 12주차에 folder 페이지를 만들면서 자연스럽게 해결하였다. 복기하자면 플로우는 다음과 같다.
1. /folder/[id]로 사용자가 접속한다.
2. 서버에서 사용자 session을 검사하여 세션이 있다면 api호출을 통해 사용자의 folderList를 받아와서 링크들을 생성하고 /folder/[id]페이지로 이동시킨다.
3. 사용자 session이 없다면 로그인 페이지로 redirect한다.
폴더 생성은 이전에 만들어 둔 폴더 추가하기 모달을 통해 구현하였다. app routing을 사용하면서 힘들었던 점이 서버 컴포넌트와 클라이언트 컴포넌트를 나누는 과정에서 컴포넌트끼리 데이터를 주고받기 힘들게 되었다. prop 드릴링으로도 잘 해결할 수 없는것이, 서버 컴포넌트에서 클라이언트 컴포넌트로 함수를 넘겨줄 수 없기 때문에 서버 컴포넌트와 클라이언트 컴포넌트를 잘 나누면서도 필요한 데이터들은 스무스하게 공유할 수 있게 만드는것이 상당히 어려웠다.
전용 ui에 버튼을 함께 넣었다. 하지만 전용 ui와 상관없는 버튼들도 있을테니, 이런 경우 modal컴포넌트의 옵셔널 prop으로 proceedBtnText를 추가하여 해당 prop이 있으면 버튼을 따로 렌더링하게 했다.
import React, { useState } from "react";
import styles from "./AddFolder.module.scss";
interface AddFolderProps {
onSubmit: (folderName: string) => void;
}
const AddFolder = ({ onSubmit }: AddFolderProps) => {
const [folderName, setFolderName] = useState("");
return (
<form className={styles.container}>
<div className={styles.inputBox}>
<input
type="text"
name="folderName"
className={styles.input}
placeholder="폴더 이름 입력"
autoComplete="off"
value={folderName}
onChange={(e) => {
setFolderName(e.target.value);
}}
/>
</div>
<input
type="submit"
className={styles.proceedBtn}
onClick={() => {
onSubmit(folderName);
}}
value="추가하기"
/>
</form>
);
};
export default AddFolder;

폴더 이름 수정또한 Create와 같이 폼을 넣어 해결하였다.
import React, { useState } from "react";
import styles from "./AddFolder.module.scss";
interface EditFolderNameProps {
folderName: string;
onSubmit: (newName: string) => void;
}
const EditFolderName = ({ folderName, onSubmit }: EditFolderNameProps) => {
const [newName, setNewName] = useState("");
return (
<form className={styles.container}>
<div className={styles.inputBox}>
<input
type="text"
name="folderName"
className={styles.input}
placeholder={folderName}
autoComplete="off"
value={newName}
onChange={(e) => {
setNewName(e.target.value);
}}
/>
</div>
<input
type="submit"
className={styles.proceedBtn}
onClick={() => {
onSubmit(newName);
}}
value="변경하기"
/>
</form>
);
};
export default EditFolderName;

폴더 삭제 또한 모달 전용 ui에는 버튼 하나만, 온클릭 핸들러를 주어 구현하였다. 온클릭 핸들러는 해당 모달 ui를 띄우는 역할을 하는 부모 컴포넌트에서 정의하여 넘겨주었다. 삭제할 폴더id에 대한 정보를 가지고 있어야 해서 부모 컴포넌트인 OptionList에 추가로 folderId를 prop으로 받게 하였다.
import React, { MouseEventHandler } from "react";
import styles from "./AddFolder.module.scss";
interface DeleteFolderProps {
onDelete: MouseEventHandler;
}
const DeleteFolder = ({ onDelete }: DeleteFolderProps) => {
return (
<>
<button type="button" className={styles.deleteBtn} onClick={onDelete}>
삭제하기
</button>
</>
);
};
export default DeleteFolder;
"use client";
...
const OptionList = ({ folderId }: OptionListProps) => {
const modalRef = useRef<HTMLDialogElement>(null);
const [modalProps, setModalProps] = useState<ModalProps>({
...ADD_FOLDER_MODAL_PROPS,
modalRef,
onClose: () => {},
});
...
// 삭제모달 안에서 삭제하기 버튼 눌렀을 때 실행할 함수
const handleDeleteFolder = async () => {
await deleteFolder(folderId);
handleCloseModal();
router.push("/folder");
};
...
// 삭제모달 open해주는 함수
const handleClickDeleteFolder = () => {
setModalProps({
...DELETE_FOLDER_MODAL_PROPS,
ui: <DeleteFolder onDelete={handleDeleteFolder} />,
onClose: handleCloseModal,
modalRef,
});
};
return (
<>
<ul className={styles.optionListContainer}>
...
<li>
// 폴더삭제 버튼
<Option
imgSrc="/deleteIcon.svg"
label="삭제"
onClick={() => {
handleClickDeleteFolder();
handleOpenModal();
}}
></Option>
</li>
</ul>
<Modal {...modalProps} />
</>
);
};
export default OptionList;
위와 같이 CRUD를 구현한 뒤, 마지막 중요한 작업이 남았다. 바로 삭제, 생성, 수정 후 사용자 화면에 바로 반영되게 해야 하는 것. 얼핏 들으면 당연히 되야 하는것 같지만 서버 컴포넌트에서 데이터를 받아오기 때문에 브라우저에서는 폴더리스트에 대한 정보를 받아오지 않는다. 그저 서버 컴포넌트가 서버사이드에서 prop으로 넘겨준 폴더리스트를 가지고 있을 뿐이다. 그래서 api호출을 통해 새 폴더를 생성하고 수정하고 삭제해도 새로고침을 하지 않는 이상 클라이언트 사이드에서는 변화가 없다.
현재 폴더를 삭제하면 /folder url로 리다이렉트 해주고 있다. 하지만 무슨 일인지 다음과 같이 /folder에서는 삭제한 것이 보이지만 /folder/[다른id]에서는 삭제한 폴더가 링크로 보이는 것을 확인할 수 있다.

위 영상에서 테스트2 폴더를 삭제한 뒤, 전체보기로 자동 이동된 후 다른 링크를 클릭해보면 테스트2폴더를 여전히 볼 수 있는것을 확인할 수 있다..
/folder로 리다이렉션 시키면서 /folder 페이지는 다시 refresh되어 최신 데이터를 가지고 있지만 나머지 링크들은 이전 데이터를 캐싱해두고 있는듯 하다. 따라서 다음과 같은 조치를 취해보기로 했다.
1. 서버 컴포넌트 최상단에서 folderList를 api호출을 통해 가져오는데, 자식 컴포넌트들에게 folderList를 넘겨준다.
2. folderList의 각 folder가 버튼 형태로 보이는 컴포넌트를 클라이언트 컴포넌트로 만들고 리코일 전역 상태를 구독하게 한다.
3. 각 모달에서 해당 전역 상태를 바꾼다. (생성, 삭제, 업데이트)
다음과 같이 코드를 변경해보았다.
// FolderChpList.tsx
const FolderChipList = ({ folderList, folderId }: FolderChipListProps) => {
const [currentFolders, setCurrentFolders] = useRecoilState(folderListState);
useEffect(() => {
setCurrentFolders(folderList);
}, [setCurrentFolders, folderList]);
return (
<>
{currentFolders.map((folder) => { // prop으로 받아온 folderList가 아닌
return (
<li key={folder.id}>
<FolderChip
href={`/folder/${folder.id}`}
label={folder.name}
selected={+folderId === folder.id}
/>
</li>
);
})}
</>
);
};
// OptionList.tsx
...
const handleDeleteFolder = async () => {
await deleteFolder(folderId);
const newFolderList = currentFolderList.filter(
(folder) => folder.id !== folderId
);
setCurrentFolderList(newFolderList); // 전역상태를 바꿔준다.
handleCloseModal();
router.push("/folder");
};
...
const handleEditFolderName = async (newName: string) => {
await updateFolderName(folderId, newName);
const newFolderList = currentFolderList.map((folder) => {
if (folder.id === folderId) {
return { ...folder, name: newName };
}
return folder;
});
setCurrentFolderList(newFolderList); // 전역상태를 바꿔준다.
handleCloseModal();
};
그랬더니 생성, 삭제, 수정 후 화면에 바로 반영이 되긴한다.

하지만 전체 탭 말고 다른 탭을 클릭했을 때 수정, 삭제, 생성 내역이 반영이 안되어있다..

확실하진 않지만,처음에 page에서 folderList를 받아오는 것이 다시 수행되지 않아서 그런것 같다. 리코일 상태가 전역적으로 바뀌기는 하지만 다른 탭에서는 캐싱된 값을 쓰고 있어서 그런것 같다... 일단 제출을 했어야 하므로 이대로 제출하였다. 다음주차에 한 번 제대로 봐야겠다