2024-04-05 80일차 React

민짱·2024년 4월 5일

📅2024. 04. 05 80일차


🎨 React_TODOLIST

todo 수정 폼 구현

  • todo 수정 폼 modal로 구현
  • defaultValue을 넣을라면 todo의 content가 필요 findTodoIndexById 함수 정의해서 찾기
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,
  };
}

todo 수정 구현

  • 수정 두 가지 버젼~
// 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);

todo 할 일 삭제 구현

  • 할 일 삭제 구현 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>
    </>
  );
}

삭제 할 때 snackbar 나오게 구현

  • TodoList에 Snackbar 추가
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}
      />
		//생략
    </>
  );
};
  • TodoOptionDrawer의 props에 openSnackbar 추가
const removeTodo = () => {
    if (confirm(`${status.todoId}번 할 일을 삭제하시겠습니까?`) == false) {
      status.close();
      return;
    }

    todosState.removeTodo(status.todoId);
    openSnackbar(true);
    status.close();
  };
  • 삭제 확인 눌렀을 때 openSnackbar(true)로 설정

삭제 확인 누르면 할 일이 삭제되었습니다. Snackbar 나옴!!!

💡 슨생님.var 그냥 아예 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,
  };
}

작성, 수정, 삭제 시 Snackbar 나오게 구현

작성 시 몇 번 todo 추가됨 Snackbar 구현

  • props 전달
 <NewTodoForm todosState={todosState} noticeSnackbarState={noticeSnackbarState} />
  • noticeSnackbarState 받고 noticeSnackbarState.open(${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 (
    <>
    //생략
    </>
  );
};

수정 시 몇 번 todo 수정됨 구현

  • props 전달
<TodoList todosState={todosState} noticeSnackbarState={noticeSnackbarState} />
  • props 받고 TodoOptionDrawer에 noticeSnackbarState 전달
const TodoList = ({ todosState, noticeSnackbarState }) => {
  const todoOptionDrawerStatus = useTodoOptionDrawerStatus();

  return (
    <>
      <TodoOptionDrawer
        status={todoOptionDrawerStatus}
        todosState={todosState}
        noticeSnackbarState={noticeSnackbarState}
      />
      //생략
    </>
  );
};
  • EditTodoModal에 noticeSnackbarState={noticeSnackbarState} 전달
function TodoOptionDrawer({ status, todosState, noticeSnackbarState }) {
  
  //생략
  
  return (
    <>
      <EditTodoModal
        status={editTodoModalStatus}
        todosState={todosState}
        todo={todo}
        noticeSnackbarState={noticeSnackbarState}
      />
     //생략
    </>
  );
}
  • EditTodoModal에 noticeSnackbarState.open(${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 수정됨`);
  };

삭제 시 몇 번 todo 삭제됨 구현

  • 수정하고 마찬가지로 TodoOptionDrawer에 props 전달
  • noticeSnackbarState.open(${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 (
    <>
      //생략
    </>
  );
}

코드펜 이용 recoil Atoms 사용

Atoms 사용 코드펜

  • recoil atoms 사용해서 결과 값 유지하자!!
  • RecoilRoot, atom, useRecoilState import
  • atoms 사용해서 page1, page2 값 스위치해도 0으로 초기화 안되게 구현

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>
	);
}

코드펜 사용해서 AtomFamily 사용해보자

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>
	);
}

리코일(Recoil), 커스텀 훅 도입, 훅에서 atom 활용, 데이터 공유

  • 커스텀 훅 도입, atom활용해서 데이터 공유 해보자!!
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) ?.을 사용하면 프로퍼티가 없는 중첩 객체를 에러 없이 안전하게 접근할 수 있다.

옵셔널 체이닝 (Optional Chaining)은 JavaScript 및 TypeScript에서 제공하는 문법으로, 객체의 속성에 접근할 때 해당 객체나 속성이 존재하지 않아도 에러를 발생시키지 않고 undefined를 반환하게 하는 기능이다.

예를 들어, 위와 같이 el.incomeId로 접근할 때, el이 undefined / null 이라면 TypeError가 발생할 것이다.

이를 방지하기 위해 && 연산자를 쓰거나, 본문에서 이야기 중인 옵셔널 체이닝을 사용하는 것이다.

  • 주의할 점
    추후에 오류를 발견하기 어렵고, 에러 디버깅 등 추후 유지보수가 좋지 않으므로 남용하지 않는 것이 중요하다.

리액트 상태관리 라이브러리

참고자료

  • 리코일 사용 => 리액트 상태관리 라이브러리 중 하나
  • 리액트 장점 : 편하다.
  • 리액트 단점 : 공유 데이터는 상위 컴포넌트에서 정의 되어야 한다.

Atoms?

  • Atoms은 Recoil의 상태를 표현하는, Recoil 에서의 상태 단위이다.

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에 저장된 모든 값이 변경되지 않도록 할 수 있음)

0개의 댓글