77일차 - react , vs code, mui, todoList 적용

·2024년 4월 2일

react, mui

MUI 테마 커스터 마이징(theme)

page.js

export default function App() {
  return (
    <>
      <ThemeProvider theme={theme}>
        <Button variant="contained">버튼</Button>
      </ThemeProvider>
    </>
  );
}

theme.js

import { createTheme } from '@mui/material';

const theme = createTheme({
  palette: {
    type: 'dark',
    primary: {
      main: '#4caf50',
    },
    secondary: {
      main: '#4caf50',
    },
  },
});

export default theme;

버튼 커스터 마이징(react-icons사용)

  • cmd에 npm install react-icons --save 다운로드
  • 그 다음 사용가능~
export default function App() {
  return (
    <>
      <ThemeProvider theme={theme}>
        <div className="tw-flex tw-items-center tw-gap-x-3">
          <Button variant="text" endIcon={<MdDeleteForever />}>
            Text
          </Button>
          <Button
            variant="contained"
            startIcon={<MdDeleteForever />}
            onClick={() => confirm('삭제할거야?')}>
            삭제
          </Button>
          <Button variant="outlined">Outlined</Button>
        </div>

        <div className="tw-flex tw-items-center tw-gap-x-3 tw-mt-3">
          <Button
            variant="text"
            onClick={() => {
              alert('버튼 클릭됨');
            }}>
            Text
          </Button>
          <Button variant="contained" disabled>
            Contained
          </Button>
          <Button variant="outlined" href="sub/">
            sub로 이동
          </Button>
        </div>
      </ThemeProvider>
    </>
  );
}

MUI App Bar 컴포넌트 fixed

export default function App() {
  return (
    <>
      <ThemeProvider theme={theme}>
        <AppBar position="fixed">
          <Toolbar>
            <div className="tw-flex-1">
              <FaBars className="tw-cursor-pointer" />
            </div>
            <div className="logo-box">
              <a href="/" className="tw-font-bold">
                NOTE!
              </a>
            </div>
            <div className="tw-flex-1 tw-flex tw-justify-end">
              <a href="/write">글쓰기</a>
            </div>
          </Toolbar>
        </AppBar>
        <Toolbar />
        <section className="tw-h-screen tw-flex tw-items-center tw-justify-center tw-text-[5rem]">
          section
        </section>
      </ThemeProvider>
    </>
  );
}

MUI snack Bar 컴포넌트, Alert

const Alert = React.forwardRef((props, ref) => {
  return <MuiAlert {...props} ref={ref} variant="filled" />;
});

export default function App() {
  const [open, setOpen] = React.useState(false);

  const alertRef = React.useRef(null);

  return (
    <>
      <ThemeProvider theme={theme}>
        <AppBar position="fixed">
          <Toolbar>
            <div className="tw-flex-1">
              <FaBars className="tw-cursor-pointer" />
            </div>
            <div className="logo-box">
              <a href="/" className="tw-font-bold">
                NOTE!
              </a>
            </div>
            <div className="tw-flex-1 tw-flex tw-justify-end">
              <a href="/write">글쓰기</a>
            </div>
          </Toolbar>
        </AppBar>
        <Toolbar />
        <section className="tw-h-screen tw-flex tw-items-center tw-justify-center tw-text-[5rem]">
          section
        </section>
      </ThemeProvider>
      <section>
        <Button onClick={() => setOpen(true)}>Open Snackbar</Button>
        <Alert ref={alertRef} severity="error" varient="filled">
          게시물이 삭제되었습니다.
        </Alert>
        <Alert severity="success" varient="outlined">
          This is a success msg!!!!!
        </Alert>
        <Snackbar
          open={open}
          autoHideDuration={2000}
          onClose={() => setOpen(false)}
          message="Note archived">
          <Alert severity="warning">게시물이 삭제됨</Alert>
        </Snackbar>
      </section>
    </>
  );
}

MUI backdrop 컴포넌트

export default function App() {
  const [open, setOpen] = React.useState(false);

  return (
    <>
      <ThemeProvider theme={theme}>
        <AppBar position="fixed">
          <Toolbar>
            <div className="tw-flex-1">
              <FaBars className="tw-cursor-pointer" />
            </div>
            <div className="logo-box">
              <a href="/" className="tw-font-bold">
                NOTE!
              </a>
            </div>
            <div className="tw-flex-1 tw-flex tw-justify-end">
              <a href="/write">글쓰기</a>
            </div>
          </Toolbar>
        </AppBar>
        <Toolbar />
        <section className="tw-h-screen tw-flex tw-items-center tw-justify-center tw-text-[5rem]">
          section
        </section>
      </ThemeProvider>
      <Button onClick={() => setOpen(true)}>Show backdrop</Button>
      <Backdrop
        sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
        open={open}
        onClick={() => setOpen(false)}>
        <CircularProgress color="inherit" />
      </Backdrop>
    </>
  );
}

MUI Drawer 컴포넌트

문제 직면

  • element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. you likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
  1. 이런식으로 에러가 뜨길래 대체 뭐지..하고 봤는데 import할때 @mui/material @mui/icons-material 이렇게 다른곳에서 import를 해야하는데 material에 한번에 import하려고 해서 오류가 났었다!
  2. 그 다음 prop-type에서 import해야하는데 material에서 또 import하려고 했다..
  3. 결론 import를 잘 했는지 꼭 확인해보자! 무작정 한꺼번에 import하려고 하진 않았는지!
export default function App() {
  const [open, setOpen] = React.useState(false);

  return (
    <>
      <ThemeProvider theme={theme}>
        <AppBar position="fixed">
          <Toolbar>
            <div className="tw-flex-1">
              <FaBars onClick={() => setOpen(true)} className="tw-cursor-pointer" />
            </div>
            <div className="logo-box">
              <a href="/" className="tw-font-bold">
                NOTE!
              </a>
            </div>
            <div className="tw-flex-1 tw-flex tw-justify-end">
              <a href="/write">글쓰기</a>
            </div>
          </Toolbar>
        </AppBar>
        <Toolbar />
        <section className="tw-h-screen tw-flex tw-items-center tw-justify-center tw-text-[5rem]">
          section
        </section>
      </ThemeProvider>
      <Button onClick={() => setOpen(true)}>show drawer</Button>
      <Drawer anchor="left" open={open} onClose={() => setOpen(false)}>
        <List>
          <ListItemButton>
            <Link href="/write">글 쓰기</Link>
          </ListItemButton>
          <ListItemButton>사과</ListItemButton>
          <ListItemButton>바나나</ListItemButton>
        </List>
      </Drawer>
    </>
  );
}

MUI Tab 컴포넌트

export default function App() {
  const [tabCurrentIndex, setTabCurrentIndex] = React.useState(0);

  return (
    <>
      <ThemeProvider theme={theme}>
        <AppBar position="fixed">
          <Toolbar>
            <div className="tw-flex-1">
              <FaBars onClick={() => setOpen(true)} className="tw-cursor-pointer" />
            </div>
            <div className="logo-box">
              <a href="/" className="tw-font-bold">
                NOTE!
              </a>
            </div>
            <div className="tw-flex-1 tw-flex tw-justify-end">
              <a href="/write">글쓰기</a>
            </div>
          </Toolbar>
        </AppBar>
        <Toolbar />
        <section className="tw-h-screen tw-flex tw-items-center tw-justify-center tw-text-[5rem]">
          section
        </section>
      </ThemeProvider>
      <Tabs value={tabCurrentIndex} onChange={(_, newValue) => setTabCurrentIndex(newValue)}>
        <Tab label="Item One" />
        <Tab label="Item Two" />
        <Tab label="Item Three" />
      </Tabs>
      {tabCurrentIndex == 0 && <div>내용1</div>}
      {tabCurrentIndex == 1 && <div>내용2</div>}
      {tabCurrentIndex == 2 && <div>내용3</div>}
    </>
  );
}

TodoList 적용, 할일 추가 폼, dateToStr

const useTodoStatus = () => {
  const [todos, setTodos] = React.useState([]);
  const lastTodoIdRef = React.useRef(0);

  const addTodo = (newTitle) => {
    const id = ++lastTodoIdRef.current;

    const newTodo = {
      id,
      title: newTitle,
      regDate: dateToStr(new Date()),
    };
    setTodos([...todos, newTodo]);
  };

  const removeTodo = (id) => {
    const newTodos = todos.filter((todo) => todo.id != id);
    setTodos(newTodos);
  };

  const modifyTodo = (id, title) => {
    const newTodos = todos.map((todo) => (todo.id != id ? todo : { ...todo, title }));
    setTodos(newTodos);
  };

  return {
    todos,
    addTodo,
    removeTodo,
    modifyTodo,
  };
};

const NewTodoForm = ({ todoStatus }) => {
  const [newTodoTitle, setNewTodoTitle] = useState('');

  const addTodo = () => {
    if (newTodoTitle.trim().length == 0) return;
    const title = newTodoTitle.trim();
    todoStatusaddTodo(title);
    setNewTodoTitle('');
  };

  return (
    <>
      <div className="flex items-center gap-x-3">
        <input
          className="input input-bordered"
          type="text"
          placeholder="새 할일 입력해"
          value={newTodoTitle}
          onChange={(e) => setNewTodoTitle(e.target.value)}
        />
        <button className="btn btn-primary" onClick={addTodo}>
          할 일 추가
        </button>
      </div>
    </>
  );
};

const TodoListItem = ({ todo, todoStatus }) => {
  const [editMode, setEditMode] = useState(false);
  const [newTodoTitle, setNewTodoTitle] = useState(todo.title);
  const readMode = !editMode;

  const enableEditMode = () => {
    setEditMode(true);
  };

  const removeTodo = () => {
    todoStatus.removeTodo(todo.id);
  };

  const cancleEdit = () => {
    setEditMode(false);
    setNewTodoTitle(todo.title);
  };
  const commitEdit = () => {
    if (newTodoTitle.trim().length == 0) return;

    todoStatus.modifyTodo(todo.id, newTodoTitle.trim());

    setEditMode(false);
  };

  return (
    <li className="flex items-center gap-x-3 mb-3">
      <span className="badge badge-accent badge-outline">{todo.id}</span>
      {readMode ? (
        <>
          <span>{todo.title}</span>
          <button className="btn btn-outline btn-accent" onClick={enableEditMode}>
            수정
          </button>
          <button className="btn btn-accent" onClick={removeTodo}>
            삭제
          </button>
        </>
      ) : (
        <>
          <input
            className="input input-bordered"
            type="text"
            placeholder="할 일 써"
            value={newTodoTitle}
            onChange={(e) => setNewTodoTitle(e.target.value)}
          />
          <button className="btn btn-accent" onClick={commitEdit}>
            수정완료
          </button>
          <button className="btn btn-accent" onClick={cancleEdit}>
            수정취소
          </button>
        </>
      )}
    </li>
  );
};

const TodoList = ({ todoStatus }) => {
  return (
    <>
      {todoStatus.todos.length == 0 ? (
        <h4>할 일 없음</h4>
      ) : (
        <>
          <h4>할 일 목록</h4>
          <ul>
            {todoStatus.todos.map((todo) => (
              <TodoListItem key={todo.id} todo={todo} todoStatus={todoStatus} />
            ))}
          </ul>
        </>
      )}
    </>
  );
};

export default function App() {
  const todoState = useTodoStatus(); // 리액트 커스텀 훅

  const onSubmit = (e) => {
    e.preventDefault();

    const form = e.currentTarget;

    form.title.value = form.title.value.trim();

    if (form.title.value.length == 0) {
      alert('할 일 써');
      form.title.focus();
      return;
    }

    todoState.addTodo(form.title.value);
    form.title.value = '';
    form.title.focus();
  };

  return (
    <>
      <ThemeProvider theme={theme}>
        <AppBar position="fixed">
          <Toolbar>
            <div className="tw-flex-1">
              <FaBars onClick={() => setOpen(true)} className="tw-cursor-pointer" />
            </div>
            <div className="logo-box">
              <a href="/" className="tw-font-bold">
                NOTE!
              </a>
            </div>
            <div className="tw-flex-1 tw-flex tw-justify-end">
              <a href="/write">글쓰기</a>
            </div>
          </Toolbar>
        </AppBar>
        <Toolbar />
        <form onSubmit={onSubmit}>
          <input type="text" name="title" autoComplete="off" placeholder="할 일 입력해" />
          <button type="submit">추가</button>
          <button type="reset">취소</button>
        </form>
        {todoState.todos.length}
      </ThemeProvider>
    </>
  );
}

// 유틸리티

// 날짜 객체 입력받아서 문장(yyyy-mm-dd hh:mm:ss)으로 반환한다.
function dateToStr(d) {
  const pad = (n) => {
    return n < 10 ? '0' + n : n;
  };

  return (
    d.getFullYear() +
    '-' +
    pad(d.getMonth() + 1) +
    '-' +
    pad(d.getDate()) +
    ' ' +
    pad(d.getHours()) +
    ':' +
    pad(d.getMinutes()) +
    ':' +
    pad(d.getSeconds())
  );
}

dateToStr 모듈화, App에 적용, theme를 App에서 분리, 공통 테마 적용

const App = () => {
  const todoState = useTodoStatus(); // 리액트 커스텀 훅

  const onSubmit = (e) => {
    e.preventDefault();

    const form = e.currentTarget;

    form.title.value = form.title.value.trim();

    if (form.title.value.length == 0) {
      alert('할 일 써');
      form.title.focus();
      return;
    }

    todoState.addTodo(form.title.value);
    form.title.value = '';
    form.title.focus();
  };

  return (
    <>
      <AppBar position="fixed">
        <Toolbar>
          <div className="tw-flex-1">
            <FaBars onClick={() => setOpen(true)} className="tw-cursor-pointer" />
          </div>
          <div className="logo-box">
            <a href="/" className="tw-font-bold">
              TODO!
            </a>
          </div>
          <div className="tw-flex-1 tw-flex tw-justify-end">
            <a href="/write">글쓰기</a>
          </div>
        </Toolbar>
      </AppBar>
      <Toolbar />
      <form onSubmit={onSubmit}>
        <input type="text" name="title" autoComplete="off" placeholder="할 일 입력해" />
        <button type="submit">추가</button>
        <button type="reset">취소</button>
      </form>
      {todoState.todos.length}
    </>
  );
};

export default function themeApp() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <App />
    </ThemeProvider>
  );
}

TODO

  • forwardRef 찾아보기
  • forwardRef
  • 리액트 복습!(useRef, useEffect, useMemo, useState)

느낀점

  • 뭔가 아직 낯선 모양이지만.. 자바스크립트라 그런지.. 뭔가 익숙한 냄새가 나지만 낯설다 후후.. 좀 더 보다보면 괜찮아질 듯 하다! 아직 에러잡아내는게 쉽지 않다 코드리뷰를 더 해보도록!
profile
우당탕탕 연이의 개발일기

1개의 댓글

comment-user-thumbnail
2024년 4월 2일

코드리뷰는 사랑입니다~

답글 달기