React

[TIL/React] 2024/08/31

✅ 나만의 로제타 스톤 구현하기MongoDB: NoSQL 데이터베이스로, JSON 형태의 데이터를 저장하고 관리한다.Express.js: Node.js 기반의 웹 애플리케이션 프레임워크로, 서버 및 API를 쉽게 구축할 수 있도록 지원한다.React.js: 사용자 인터

2024년 8월 31일9min read

✅ 나만의 로제타 스톤 구현하기

0. 원하는 기술 스택 👨‍💻

MongoDB```: NoSQL 데이터베이스로, JSON 형태의 데이터를 저장하고 관리한다.
React.js```: 사용자 인터페이스를 구축하기 위한 JavaScript 라이브러리로, 컴포넌트 기반의 UI 개발을 지원한다.
Material-UI```: React용 UI 컴포넌트 라이브러리로, Google의 Material Design을 기반으로 한 디자인 시스템을 제공한다.
React-Query```: React 애플리케이션의 서버 상태를 관리하기 위한 라이브러리로, 데이터 fetching, caching, syncing을 지원한다.

(금일 목표: 내가 구현하길 원하는 기술 스택 조합을 아우르는, ```가장 간단한 형태의 로제타 스톤```을 만들어보자!)

## 1. Backend 👨‍💻

1-1. 초기화 및 패키지 설치 ✍️

code
// 프로젝트 초기화 관련 command line
mkdir todolist-app
cd todolist-app
mkdir backend
cd backend
yarn init -y

// 패키지 설치 관련 command line
yarn add express mongoose dotenv cors
yarn add -D nodemon

1-2. server.js 설정 ✍️

code
const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
require("dotenv").config();

// Express 애플리케이션을 생성한다.
const app = express();
const PORT = process.env.PORT || 5000;

// CORS를 설정하여 모든 출처의 요청을 허용한다.
app.use(cors());

// JSON 형식의 요청 본문을 파싱한다.
app.use(express.json());

// MongoDB 데이터베이스에 연결한다.
mongoose
  .connect(process.env.MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true, // 새로운 서버 탐지 및 모니터링 엔진 사용
  })
  .then(() => console.log("MongoDB connected")) // 연결 성공 시 메시지 출력
  .catch((err) => console.log(err)); // 연결 실패 시 에러 출력

// '/api/todos' 경로로 들어오는 요청을 처리할 라우트를 등록한다.
// todos 라우트 파일을 가져와서 처리
app.use("/api/todos", require("./routes/todos"));

// 서버가 지정된 포트에서 요청을 수신한다.
// 서버가 성공적으로 시작되었음을 출력
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

1-3. MongoDB 모델 생성 ✍️

code
const mongoose = require("mongoose");

// Todo 스키마 정의
const TodoSchema = new mongoose.Schema({
  title: {
    type: String, // 제목은 문자열 타입
    required: true, // 제목은 필수 입력 항목
  },
  completed: {
    type: Boolean, // 완료 상태는 불리언 타입
    default: false, // 기본값은 false (할 일이 완료되지 않은 상태로 시작한다는 뜻)
  },
});

// 'Todo' 모델을 정의하고, TodoSchema를 사용하여 Mongoose 모델을 생성
module.exports = mongoose.model("Todo", TodoSchema);

1-4. Controller 생성 ✍️

code
const Todo = require("../models/Todo");

// 모든 Todo 항목을 가져오는 함수
exports.getTodos = async (req, res) => {
  try {
    const todos = await Todo.find(); // 모든 Todo 항목을 MongoDB에서 찾음
    res.json(todos); // 결과를 JSON 형식으로 클라이언트에 응답
  } catch (err) {
    res.status(500).json({ message: "Error fetching todos" });
  }
};

// 새로운 Todo 항목을 생성하는 함수
exports.createTodo = async (req, res) => {
  try {
    const newTodo = new Todo({
      title: req.body.title, // 요청 본문에서 제목을 가져옴
      completed: req.body.completed, // 요청 본문에서 완료 상태를 가져옴
    });
    const savedTodo = await newTodo.save(); // 새 Todo 항목을 데이터베이스에 저장
    res.json(savedTodo); // 저장된 Todo 항목을 JSON 형식으로 클라이언트에 응답
  } catch (err) {
    res.status(500).json({ message: "Error creating todo" });
  }
};

// 특정 Todo 항목을 업데이트하는 함수
exports.updateTodo = async (req, res) => {
  try {
    const updatedTodo = await Todo.findByIdAndUpdate(req.params.id, req.body, {
      new: true, // 업데이트된 결과를 반환
    });
    res.json(updatedTodo); // 업데이트된 Todo 항목을 JSON 형식으로 클라이언트에 응답
  } catch (err) {
    res.status(500).json({ message: "Error updating todo" });
  }
};

// 특정 Todo 항목을 삭제하는 함수
exports.deleteTodo = async (req, res) => {
  try {
    await Todo.findByIdAndDelete(req.params.id); // 요청 경로의 ID를 가진 Todo 항목을 삭제
    res.json({ message: "Todo deleted" }); // 삭제 성공 메시지를 JSON 형식으로 클라이언트에 응답
  } catch (err) {
    res.status(500).json({ message: "Error deleting todo" });
  }
};

1-5. Router 설정 ✍️

code
const express = require("express");

const {
  getTodos,
  createTodo,
  updateTodo,
  deleteTodo,
} = require("../controllers/todoController");

// Express 라우터를 생성
const router = express.Router();

// GET 요청이 '/' 경로로 들어올 때, 모든 Todo 항목을 가져오는 핸들러를 호출
router.get("/", getTodos);

// POST 요청이 '/' 경로로 들어올 때, 새 Todo 항목을 생성하는 핸들러를 호출
router.post("/", createTodo);

// PUT 요청이 '/:id' 경로로 들어올 때, 특정 Todo 항목을 업데이트하는 핸들러를 호출
router.put("/:id", updateTodo);

// DELETE 요청이 '/:id' 경로로 들어올 때, 특정 Todo 항목을 삭제하는 핸들러를 호출
router.delete("/:id", deleteTodo);

// 설정한 라우터를 모듈로 내보냄
module.exports = router;

1-6. env 및 nodemon 설정 ✍️

code
// .env 설정 양식
MONGO_URI=your_mongodb_uri_here

// nodemon 관련 설정을 package.json에 추가
"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

2. Frontend 👨‍💻

MERN stack Frontend 셋팅을 6단계로 나누어보자.```

### 2-1. 디렉토리 및 패키지 설정 ✍️

// vite project 생성 mkdir frontend cd frontend yarn create vite .

// 패키지 설정 yarn install yarn add @mui/material jotai react-query axios

code

### 2-2. main.jsx 설정 ✍️

import ReactDOM from "react-dom/client"; import App from "./App"; // React-Query에서 QueryClient와 QueryClientProvider를 가져옴 import { QueryClient, QueryClientProvider } from "react-query";

// 새로운 QueryClient 인스턴스를 생성 // QueryClient는 React-Query의 데이터 캐싱 및 상태 관리를 처리 const queryClient = new QueryClient();

// React 애플리케이션을 DOM에 렌더링 ReactDOM.createRoot(document.getElementById("root")).render( // QueryClientProvider를 사용하여 전체 애플리케이션에 QueryClient를 제공 );

code

### 2-3. App.jsx 설정 ✍️

// TodoList Component 렌더링 import TodoList from "./components/TodoList"; import { Container } from "@mui/material";

const App = () => { return ( ); };

export default App;

code

### 2-4. atom 설정(jotai) ✍️

// src/atoms.js import { atom } from "jotai";

const todoListAtom = atom([]); const newTodoAtom = atom(""); const editTodoAtom = atom(null); const editTodoTitleAtom = atom("");

export { todoListAtom, newTodoAtom, editTodoAtom, editTodoTitleAtom };

code

### 2-5. CRUD를 위한 hook 설정(React-Query, Axios) ✍️

// src/hooks/useTodos.js import { useQuery, useMutation, useQueryClient } from "react-query"; import axios from "axios"; import { useAtom } from "jotai"; import { todoListAtom } from "../atoms/todoAtom";

// Todo 항목을 가져오는 비동기 함수 const fetchTodos = async () => { // API에서 Todo 항목을 가져옴 const { data } = await axios.get("http://localhost:5000/api/todos"); // 가져온 데이터를 반환 return data; };

// 새로운 Todo 항목을 생성하는 비동기 함수 const createTodo = async (newTodo) => { // API에 새로운 Todo 항목을 POST 요청으로 보냄 const { data } = await axios.post("http://localhost:5000/api/todos", newTodo); // 생성된 Todo 항목을 반환 return data; };

// Todo 항목을 업데이트하는 비동기 함수 const updateTodo = async (updatedTodo) => { // API에 PUT 요청으로 Todo 항목을 업데이트 const { data } = await axios.put( http://localhost:5000/api/todos/${updatedTodo._id}, updatedTodo ); // 업데이트된 Todo 항목을 반환 return data; };

// Todo 항목을 삭제하는 비동기 함수 const deleteTodo = async (id) => { // API에 DELETE 요청으로 Todo 항목을 삭제 await axios.delete(http://localhost:5000/api/todos/${id}); };

// Todo 항목을 가져오기 위한 커스텀 훅 export const useTodos = () => { const [, setTodos] = useAtom(todoListAtom); // 전역 상태의 todos를 가져옴 return useQuery("todos", fetchTodos, { // 서버에서 데이터를 가져온 후 전역 상태를 업데이트 onSuccess: (data) => setTodos(data), refetchOnWindowFocus: false, }); };

// 새로운 Todo 항목을 생성하기 위한 커스텀 훅 export const useCreateTodo = () => { const queryClient = useQueryClient(); return useMutation(createTodo, { // Todo 항목이 성공적으로 생성되면 'todos' 쿼리를 무효화하여 데이터를 새로고침 onSuccess: () => { queryClient.invalidateQueries("todos"); queryClient.refetchQueries("todos"); }, }); };

// Todo 항목을 업데이트하기 위한 커스텀 훅 export const useUpdateTodo = () => { const queryClient = useQueryClient(); return useMutation(updateTodo, { // Todo 항목이 성공적으로 업데이트되면 'todos' 쿼리를 무효화하여 데이터를 새로고침 onSuccess: () => { queryClient.invalidateQueries("todos"); queryClient.refetchQueries("todos"); }, }); };

// Todo 항목을 삭제하기 위한 커스텀 훅 export const useDeleteTodo = () => { const queryClient = useQueryClient(); return useMutation(deleteTodo, { // Todo 항목이 성공적으로 삭제되면 'todos' 쿼리를 무효화하여 데이터를 새로고침 onSuccess: () => { queryClient.invalidateQueries("todos"); queryClient.refetchQueries("todos"); }, }); };

code

### 2-6. 예제 component 설정 ✍️

// src/components/TodoList.jsx import { useAtom } from "jotai"; import { todoListAtom, newTodoAtom, editTodoAtom, editTodoTitleAtom, } from "../atoms/todoAtom"; import { Box, Button, TextField, Checkbox, List, ListItem, ListItemText, IconButton, Container, } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import CheckIcon from "@mui/icons-material/Check"; import CloseIcon from "@mui/icons-material/Close"; import { useTodos, useCreateTodo, useUpdateTodo, useDeleteTodo, } from "../hooks/useTodos";

const TodoList = () => { const [todos] = useAtom(todoListAtom); // 전역 상태에서 todos 가져오기 const [newTodo, setNewTodo] = useAtom(newTodoAtom); // 전역 상태에서 새 Todo 제목 가져오기 const [editTodoId, setEditTodoId] = useAtom(editTodoAtom); // 전역 상태에서 편집 중인 Todo의 ID 가져오기 const [editTodoTitle, setEditTodoTitle] = useAtom(editTodoTitleAtom); // 전역 상태에서 편집 중인 Todo 제목 가져오기

const { isLoading } = useTodos(); // 데이터와 로딩 상태 가져오기 const createTodo = useCreateTodo(); const updateTodo = useUpdateTodo(); const deleteTodo = useDeleteTodo();

if (isLoading) return

Loading...
; // 로딩 중일 때 로딩 메시지 표시

const handleAddTodo = () => { if (newTodo.trim()) { createTodo.mutate({ title: newTodo }); // 새 Todo 항목을 서버에 전송 setNewTodo(""); // 입력 필드 초기화 } };

const handleEditTodo = (todo) => { setEditTodoId(todo._id); // 편집 중인 Todo의 ID 설정 setEditTodoTitle(todo.title); // 편집 중인 Todo 제목 설정 };

const handleUpdateTodo = (todo) => { updateTodo.mutate({ ...todo, title: editTodoTitle }); // Todo 제목 업데이트 setEditTodoId(null); // 편집 모드 종료 setEditTodoTitle(""); // 제목 입력 필드 초기화 };

const handleCancelEdit = () => { setEditTodoId(null); // 편집 중인 Todo의 ID 초기화 setEditTodoTitle(""); // 제목 입력 필드 초기화 };

return ( {/ 제목 /}

먹킷리스트

{/ 새 Todo 항목 추가 섹션 /} setNewTodo(e.target.value)} label="먹고 싶은거 추가하셈(MERN Test용)" variant="outlined" fullWidth />

{/ Todo 항목 목록 섹션 /} {todos.map((todo) => ( {/ 완료 상태를 표시하는 체크박스 /} updateTodo.mutate({ ...todo, completed: !todo.completed }) } />

{/ Todo 항목 제목: 편집 모드일 때와 아닐 때 /} {editTodoId === todo._id ? ( setEditTodoTitle(e.target.value)} variant="outlined" size="small" fullWidth /> ) : ( )}

{/ 편집 모드일 때와 아닐 때의 버튼들 /} {editTodoId === todo._id ? ( <> handleUpdateTodo(todo)}> ) : ( <> handleEditTodo(todo)}> deleteTodo.mutate(todo._id)} > )} ))} ); };

export default TodoList;

code

## 3. 완성본 이미지 👨‍💻

![](https://velog.velcdn.com/images/minkwan/post/f891cee2-6724-44a6-9058-f9c6d3648f80/image.png)

![](https://velog.velcdn.com/images/minkwan/post/9c4de3b0-1a44-4ee2-b177-40637cbeb7f5/image.png)


## 4. 분석 방향과 앞으로의 고민 👨‍💻

### 4-1. 분석 ✍️

2-4. atom 설정(jotai) 2-5. CRUD를 위한 hook 설정(React-Query, Axios) 2-6. 예제 component 설정

1) 4와 5 2) 5와 6 3) 4와 6 5) 4와 5와 6

backend의 대응관계```는 ```11가지```다.

1-2. server.js 설정
1-3. MongoDB 모델 생성
1-4. Controller 생성
1-5. Router 설정

1) 2와 3
2) 2와 4 
3) 2와 5
5) 3과 4
6) 3과 5
7) 4와 5
8) 2와 3과 4
9) 2와 3과 5
10) 2와 4와 5
11) 2와 3과 4와 5

4-2. 고민 ✍️