📅2024. 04. 05 80일차
function EditTodoModal({ status, todosState, todo }) {
const onSubmit = (e) => {
e.preventDefault();
const form = e.currentTarget;
form.content.value = form.content.value.trim();
if (form.content.value.length == 0) {
alert('할 일 써');
form.content.focus();
return;
}
};
return (
<>
<Modal
open={status.opened}
onClose={status.close}
className="tw-flex tw-justify-center tw-items-center">
<div className="tw-bg-white tw-p-10 tw-rounded-[20px] tw-w-full tw-max-w-lg">
<form onSubmit={onSubmit} className="tw-flex tw-flex-col tw-gap-2">
<TextField
minRows={3}
maxRows={10}
multiline
name="content"
autoComplete="off"
variant="outlined"
label="할 일 써"
defaultValue={todo?.content}
/>
<Button variant="contained" className="tw-font-bold" type="submit">
수정
</Button>
</form>
</div>
</Modal>
</>
);
}
const findTodoIndexById = (id) => {
return todos.findIndex((todo) => todo.id == id);
};
const findTodoById = (id) => {
const index = findTodoIndexById(id);
if (index == -1) {
return null;
}
return todos[index];
};
return {
todos,
addTodo,
removeTodo,
modifyTodo,
findTodoById,
};
}
// modify v1
const modifyTodo = (id, content) => {
const newTodos = todos.map((todo) => (todo.id != id ? todo : { ...todo, content }));
setTodos(newTodos);
};
// modify v2
const modifyTodoByIndex = (index, newContent) => {
const newTodos = todos.map((todo, _index) =>
_index != index ? todo : { ...todo, content: newContent },
);
setTodos(newTodos);
};
// modify v2
const modifyTodoById = (id, newContent) => {
const index = findTodoIndexById(id);
if (index == -1) {
return null;
}
modifyTodoByIndex(index, newContent);
};
// modify v1
todosState.modifyTodo(todo.id, form.content.value);
status.close();
// modify v2
// todosState.modifyTodoById(todo.id, form.content.value);
할 일 삭제 구현 removeTodo 컴포넌트
삭제는 다른 폼 없이 그냥 Drawer에서 바로 삭제 가능하게 하면 될 듯
Drawer의 삭제 버튼에 onClick={removeTodo} 넣기
removeTodo 함수 선언해서 삭제하게 만듬
const removeTodo = (id) => {
const newTodos = todos.filter((todo) => todo.id != id);
setTodos(newTodos);
};
function TodoOptionDrawer({ status, todosState }) {
const removeTodo = () => {
if (confirm(`${status.todoId}번 할 일을 삭제하시겠습니까?`) == false) {
status.close();
return;
}
todosState.removeTodo(status.todoId);
status.close();
};
//생략
return (
<>
<EditTodoModal status={editTodoModalStatus} todosState={todosState} todo={todo} />
<SwipeableDrawer anchor="top" open={status.opened} onClose={status.close} onOpen={() => {}}>
<List>
<ListItem className="tw-flex tw-gap-2 tw-p-[15px]">
<span className="tw-text-[--mui-color-primary-main]">{status.todoId}번 </span>
<span>Your Todo</span>
</ListItem>
<Divider className="tw-my-[5px]" />
//생략
<ListItemButton
className="tw-p-[15px_20px] tw-flex tw-gap-2 tw-items-center"
onClick={removeTodo}>
<span>삭제</span>
<FaTrash className="block tw-mt-[-5px]" />
</ListItemButton>
</List>
</SwipeableDrawer>
</>
);
}
const TodoList = ({ todosState }) => {
//생략
const [snackbarOpen, setSnackbarOpen] = React.useState(false);
const openSnackbar = () => {
setSnackbarOpen(true);
};
const closeSnackbar = () => {
setSnackbarOpen(false);
};
return (
<>
<Snackbar open={snackbarOpen} autoHideDuration={4000} onClose={closeSnackbar}>
<Alert variant="filled" severity="success" onClose={closeSnackbar}>
할 일이 삭제되었습니다.
</Alert>
</Snackbar>
<TodoOptionDrawer
status={todoOptionDrawerStatus}
todosState={todosState}
openSnackbar={openSnackbar}
/>
//생략
</>
);
};
const removeTodo = () => {
if (confirm(`${status.todoId}번 할 일을 삭제하시겠습니까?`) == false) {
status.close();
return;
}
todosState.removeTodo(status.todoId);
openSnackbar(true);
status.close();
};
삭제 확인 누르면 할 일이 삭제되었습니다. Snackbar 나옴!!!
function NoticeSnackbar({ status }) {
return (
<>
<Snackbar
open={status.opened}
autoHideDuration={status.autoHideDuration}
onClose={status.close}>
<Alert variant={status.variant} severity={status.severity}>
{status.msg}
</Alert>
</Snackbar>
</>
);
}
function useNoticeSnackbarStatus() {
const [opened, setOpened] = React.useState(false);
const [autoHideDuration, setAutoHideDuration] = React.useState(null);
const [variant, setVariant] = React.useState(null);
const [severity, setSeverity] = React.useState(null);
const [msg, setMsg] = React.useState(null);
const open = (msg, severity = 'success', autoHideDuration = 1000, variant = 'filled') => {
setOpened(true);
setMsg(msg);
setSeverity(severity);
setAutoHideDuration(autoHideDuration);
setVariant(variant);
};
const close = () => {
setOpened(false);
};
return {
opened,
open,
close,
autoHideDuration,
variant,
severity,
msg,
};
}
<NewTodoForm todosState={todosState} noticeSnackbarState={noticeSnackbarState} />
${newTodoId}번 todo 추가됨); 전달const NewTodoForm = ({ todosState, noticeSnackbarState }) => {
const onSubmit = (e) => {
e.preventDefault();
const form = e.currentTarget;
form.content.value = form.content.value.trim();
if (form.content.value.length == 0) {
alert('할 일 써');
form.content.focus();
return;
}
const newTodoId = todosState.addTodo(form.content.value);
form.content.value = '';
form.content.focus();
noticeSnackbarState.open(`${newTodoId}번 todo 추가됨`);
};
return (
<>
//생략
</>
);
};
<TodoList todosState={todosState} noticeSnackbarState={noticeSnackbarState} />
const TodoList = ({ todosState, noticeSnackbarState }) => {
const todoOptionDrawerStatus = useTodoOptionDrawerStatus();
return (
<>
<TodoOptionDrawer
status={todoOptionDrawerStatus}
todosState={todosState}
noticeSnackbarState={noticeSnackbarState}
/>
//생략
</>
);
};
function TodoOptionDrawer({ status, todosState, noticeSnackbarState }) {
//생략
return (
<>
<EditTodoModal
status={editTodoModalStatus}
todosState={todosState}
todo={todo}
noticeSnackbarState={noticeSnackbarState}
/>
//생략
</>
);
}
${todo.id}번 todo 수정됨); 추가function EditTodoModal({ status, todosState, todo, noticeSnackbarState }) {
const onSubmit = (e) => {
e.preventDefault();
const form = e.currentTarget;
form.content.value = form.content.value.trim();
if (form.content.value.length == 0) {
alert('할 일 써');
form.content.focus();
return;
}
// modify v1
todosState.modifyTodo(todo.id, form.content.value);
status.close();
noticeSnackbarState.open(`${todo.id}번 todo 수정됨`);
};
${status.todoId}번 todo 삭제됨, 'error'); 추가function TodoOptionDrawer({ status, todosState, noticeSnackbarState }) {
const removeTodo = () => {
if (confirm(`${status.todoId}번 할 일을 삭제하시겠습니까?`) == false) {
status.close();
return;
}
todosState.removeTodo(status.todoId);
status.close();
noticeSnackbarState.open(`${status.todoId}번 todo 삭제됨`, 'error');
};
//생략
return (
<>
//생략
</>
);
}
import React, { useState } from "https://cdn.skypack.dev/react@18";
import ReactDOM from "https://cdn.skypack.dev/react-dom@18";
import {
RecoilRoot,
atom,
useRecoilState
} from "https://cdn.skypack.dev/recoil";
const page1NoAtom = atom({
key: 'app/page1NoAtom',
default: 0,
});
function Page1() {
const [no, setNo] = useRecoilState(page1NoAtom);
return (
<>
<h3>페이지1</h3>
<ul>
<li>페이지 1의 숫자 : {no}</li>
<li>
<button onClick={() => setNo(no + 1)}>페이지 1의 숫자 증가</button>
</li>
<li>
<button onClick={() => setNo(no - 1)}>페이지 1의 숫자 감소</button>
</li>
</ul>
</>
);
}
const page2NoAtom = atom({
key: 'app/page2NoAtom',
default: 0,
});
function Page2() {
const [no, setNo] = useRecoilState(page2NoAtom);
return (
<>
<h3>페이지2</h3>
<ul>
<li>페이지 2의 숫자 : {no}</li>
<li>
<button onClick={() => setNo(no + 1)}>페이지 2의 숫자 증가</button>
</li>
<li>
<button onClick={() => setNo(0)}>페이지 2 숫자 초기화</button>
</li>
</ul>
</>
);
}
const App = () => {
const [pageMenu, setPageMenu] = useState("page1");
const switchPage = () => {
setPageMenu(pageMenu == "page1" ? "page2" : "page1");
};
return (
<>
<button onClick={switchPage}>스위치</button>
{pageMenu == "page1" && <Page1 />}
{pageMenu == "page2" && <Page2 />}
</>
);
};
function Root() {
return (
<RecoilRoot>
<App />
</RecoilRoot>
);
}
import React, { useState } from "https://cdn.skypack.dev/react@18";
import ReactDOM from "https://cdn.skypack.dev/react-dom@18";
import {
RecoilRoot,
atom,
atomFamily,
useRecoilState
} from "https://cdn.skypack.dev/recoil";
const pageNoAtomFamily = atomFamily({
key: "app/pageNoAtomFamily",
default: (no) => 0
});
function Page1() {
const [no, setNo] = useRecoilState(pageNoAtomFamily(1));
return (
<>
<h3>페이지1</h3>
<ul>
<li>페이지 1의 숫자 : {no}</li>
<li>
<button onClick={() => setNo(no + 1)}>증가</button>
</li>
</ul>
</>
);
}
function Page2() {
const [no, setNo] = useRecoilState(pageNoAtomFamily(2));
return (
<>
<h3>페이지2</h3>
<ul>
<li>페이지 2의 숫자 : {no}</li>
<li>
<button onClick={() => setNo(no + 1)}>증가</button>
</li>
</ul>
</>
);
}
function Page3() {
const [no, setNo] = useRecoilState(pageNoAtomFamily(2));
return (
<>
<h3>페이지3</h3>
<ul>
<li>페이지 3의 숫자 : {no}</li>
<li>
<button onClick={() => setNo(no + 1)}>증가</button>
</li>
</ul>
</>
);
}
function Page4() {
const [no, setNo] = useRecoilState(pageNoAtomFamily(1));
return (
<>
<h3>페이지4(페이지1의 데이터를 공유 받음)</h3>
<ul>
<li>페이지 4의 숫자 : {no}</li>
<li>
<button onClick={() => setNo(no + 1)}>증가</button>
</li>
</ul>
</>
);
}
const App = () => {
const [pageNo, setPageNo] = useState(1);
const switchPage = () => {
setPageNo(pageNo + 1 <= 4 ? pageNo + 1 : 1);
};
const pageName = "page" + pageNo;
return (
<>
<button onClick={switchPage}>스위치</button>
{pageName == "page1" && <Page1 />}
{pageName == "page2" && <Page2 />}
{pageName == "page3" && <Page3 />}
{pageName == "page4" && <Page4 />}
</>
);
};
function Root() {
return (
<RecoilRoot>
<App />
</RecoilRoot>
);
}
import React, { useState } from "https://cdn.skypack.dev/react@18";
import ReactDOM from "https://cdn.skypack.dev/react-dom@18";
import {
RecoilRoot,
atom,
atomFamily,
useRecoilState
} from "https://cdn.skypack.dev/recoil";
const pageNoAtomFamily = atomFamily({
key: "app/pageNoAtomFamily",
default: (no) => 0
});
const pageCountAtom = atom({
key: "app/pageCountAtom",
default: 0
});
function usePageCountStatus() {
const [count, setCount] = useRecoilState(pageCountAtom);
const increaseOne = () => setCount(count + 1);
const decreaseOne = () => setCount(count - 1);
const increaseTen = () => setCount(count + 10);
const decreaseTen = () => setCount(count - 10);
const clear = () => setCount(0);
return {
count,
increaseOne,
decreaseOne,
increaseTen,
decreaseTen,
clear
};
}
function Page1() {
const pageCountStatus = usePageCountStatus();
return (
<>
<h3>페이지1</h3>
<ul>
<li>페이지 1의 숫자 : {pageCountStatus.count}</li>
<li>
<button onClick={pageCountStatus.increaseOne}>1 증가</button>
<button onClick={pageCountStatus.decreaseOne}>1 감소</button>
<button onClick={pageCountStatus.increaseTen}>10 증가</button>
<button onClick={pageCountStatus.decreaseTen}>10 감소</button>
<button onClick={pageCountStatus.clear}>초기화</button>
</li>
</ul>
</>
);
}
function Page2() {
const pageCountStatus = usePageCountStatus();
return (
<>
<h3>페이지2</h3>
<ul>
<li>페이지 2의 숫자 : {pageCountStatus.count}</li>
<li>
<button onClick={pageCountStatus.increaseOne}>1 증가</button>
<button onClick={pageCountStatus.decreaseOne}>1 감소</button>
<button onClick={pageCountStatus.increaseTen}>10 증가</button>
<button onClick={pageCountStatus.decreaseTen}>10 감소</button>
<button onClick={pageCountStatus.clear}>초기화</button>
</li>
</ul>
</>
);
}
function Page3() {
const pageCountStatus = usePageCountStatus();
return (
<>
<h3>페이지3</h3>
<ul>
<li>페이지 3의 숫자 : {pageCountStatus.count}</li>
<li>
<button onClick={pageCountStatus.increaseOne}>1 증가</button>
<button onClick={pageCountStatus.decreaseOne}>1 감소</button>
<button onClick={pageCountStatus.increaseTen}>10 증가</button>
<button onClick={pageCountStatus.decreaseTen}>10 감소</button>
<button onClick={pageCountStatus.clear}>초기화</button>
</li>
</ul>
</>
);
}
function Page4() {
const pageCountStatus = usePageCountStatus();
return (
<>
<h3>페이지4</h3>
<ul>
<li>페이지 4의 숫자 : {pageCountStatus.count}</li>
<li>
<button onClick={pageCountStatus.increaseOne}>1 증가</button>
<button onClick={pageCountStatus.decreaseOne}>1 감소</button>
<button onClick={pageCountStatus.increaseTen}>10 증가</button>
<button onClick={pageCountStatus.decreaseTen}>10 감소</button>
<button onClick={pageCountStatus.clear}>초기화</button>
</li>
</ul>
</>
);
}
const App = () => {
const [pageNo, setPageNo] = useState(1);
const switchPage = () => {
setPageNo(pageNo + 1 <= 4 ? pageNo + 1 : 1);
};
const pageName = "page" + pageNo;
return (
<>
<button onClick={switchPage}>스위치</button>
{pageName == "page1" && <Page1 />}
{pageName == "page2" && <Page2 />}
{pageName == "page3" && <Page3 />}
{pageName == "page4" && <Page4 />}
</>
);
};
function Root() {
return (
<RecoilRoot>
<App />
</RecoilRoot>
);
}
옵셔널 체이닝 (Optional Chaining)은 JavaScript 및 TypeScript에서 제공하는 문법으로, 객체의 속성에 접근할 때 해당 객체나 속성이 존재하지 않아도 에러를 발생시키지 않고 undefined를 반환하게 하는 기능이다.
예를 들어, 위와 같이 el.incomeId로 접근할 때, el이 undefined / null 이라면 TypeError가 발생할 것이다.
이를 방지하기 위해 && 연산자를 쓰거나, 본문에서 이야기 중인 옵셔널 체이닝을 사용하는 것이다.
- 주의할 점
추후에 오류를 발견하기 어렵고, 에러 디버깅 등 추후 유지보수가 좋지 않으므로 남용하지 않는 것이 중요하다.
- 리코일 사용 => 리액트 상태관리 라이브러리 중 하나
- 리액트 장점 : 편하다.
- 리액트 단점 : 공유 데이터는 상위 컴포넌트에서 정의 되어야 한다.
Atoms이 업데이트되면 해당 Atom을 구독하고 있던 모든 컴포넌트가 업데이트된 Atom값을 참조하여 리렌더링 된다.
Atoms은 다음과 같이 Key,Value 값을 가지고 있으며 (es6의 Map을 사용한다고 함) 각 Atom은 자신의 고유한 Key 값으로 구분된다.
따라서 Atoms은 App 전체에서 다른 Atom과 Selector와 구분되는 자신만의 Key값을 가져야한다.
const counter = atom({
key: 'myCounter',
default: 0,
});
Atoms 사용법
Recoil Atoms을 생성하기 위해서는 atom() 함수를 호출해야하며 atom() 함수는 Writeable 한 RecoilState 객체를 반환한다.
function atom<T>({
key: string,
default: T | Promise<T> | RecoilValue<T>,
effects_UNSTABLE?: $ReadOnlyArray<AtomEffect<T>>,
dangerouslyAllowMutability?: boolean,
}): RecoilState<T>
- atom() 함수 옵션
key : 위에 설명 함
default : atom의 초깃값 또는 다른 atom 이나 selector 또는 Promise 객체
effects_UnStable : atom을 위한 Atom Effects 배열
dangerouslyAllowMutablity : atom의 값이 변경될 경우 등록된 컴포넌트에게 알리지 않고 리렌더링 되는 경우를 방지하기 위한 옵션 (atom에 저장된 모든 값이 변경되지 않도록 할 수 있음)