[TIL/React] 2024/08/31
✅ 나만의 로제타 스톤 구현하기MongoDB: NoSQL 데이터베이스로, JSON 형태의 데이터를 저장하고 관리한다.Express.js: Node.js 기반의 웹 애플리케이션 프레임워크로, 서버 및 API를 쉽게 구축할 수 있도록 지원한다.React.js: 사용자 인터
✅ 나만의 로제타 스톤 구현하기
0. 원하는 기술 스택 👨💻
(금일 목표: 내가 구현하길 원하는 기술 스택 조합을 아우르는, ```가장 간단한 형태의 로제타 스톤```을 만들어보자!)
## 1. Backend 👨💻
1-1. 초기화 및 패키지 설치 ✍️
// 프로젝트 초기화 관련 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 설정 ✍️
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 모델 생성 ✍️
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 생성 ✍️
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 설정 ✍️
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 설정 ✍️
// .env 설정 양식
MONGO_URI=your_mongodb_uri_here
// nodemon 관련 설정을 package.json에 추가
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
2. Frontend 👨💻
### 2-1. 디렉토리 및 패키지 설정 ✍️
// vite project 생성 mkdir frontend cd frontend yarn create vite .
// 패키지 설정 yarn install yarn add @mui/material jotai react-query axios
### 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를 제공
### 2-3. App.jsx 설정 ✍️
// TodoList Component 렌더링 import TodoList from "./components/TodoList"; import { Container } from "@mui/material";
const App = () => { return (
export default App;
### 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 };
### 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"); }, }); };
### 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
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 항목 추가 섹션 /}
{/ Todo 항목 목록 섹션 /} {todos.map((todo) => (
{/ Todo 항목 제목: 편집 모드일 때와 아닐 때 /} {editTodoId === todo._id ? (
{/ 편집 모드일 때와 아닐 때의 버튼들 /} {editTodoId === todo._id ? ( <>
export default TodoList;
## 3. 완성본 이미지 👨💻


## 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
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. 고민 ✍️
More to read
Amazon VPC Architecture 이해하기
새로운 프로젝트를 기획하며, 개발에서 무엇을 가장 먼저 고민해야 하는지 다시 돌아보게 되었습니다.한때는 프론트엔드가 모든 설계의 출발점이라고 믿었습니다. 유저가 무엇을 보고, 어떤 흐름에서 머무르고 이탈하는지에 대한 이해 없이 서비스를 만든다는 건 불가능하다고 생각했기
'원사이트'프론트엔드 관점으로 알고리즘 이해하기
오랜만에 방법론에 관한 글을 쓰게 되었습니다. 최근 상황은 이렇습니다. SSAFY에서는 하루에 엄청난 양의 알고리즘 문제들을 과제로 수행하게 됩니다. 그 과정에서, '구현력'이 매우 떨어진다는 생각이 들었습니다. 완전히 어려운 문제라면 '아쉬움'이라는 감정조차 느끼지
SubnetVPC 설계의 시작: IP와 Subnet
반복되는 루틴 속에서 얻은 안정감을 발판 삼아, 이제는 기술적 스펙트럼을 넓히기 위한 개인 프로젝트에 착수하고자 합니다.이번 프로젝트의 목표는 단순한 포트폴리오 구축을 넘어, 실제 서비스 수준의 블로그 시스템 구현과 다국어 처리 적용 등 실무에 가까운 역량을 한 단계