courses
기본 할 일 목록은 처음에는 잘 작동합니다. 작업을 추가하고 완료 후 닫으면 됩니다. 하지만 작업이 계속 늘어나면 사용하기가 점점 불편해집니다. 이를 해결하는 한 가지 방법은 카테고리를 추가하는 것입니다. 각 작업이 업무, 학습, 개인 등과 같은 카테고리에 속하도록 하고, 목록을 해당 카테고리로 필터링할 수 있습니다.
이 튜토리얼에서는 Node.js와 MongoDB를 사용하여 작업을 카테고리별로 정리할 수 있는 REST API를 만드는 방법을 배웁니다. 마지막에는 생성, 목록, 조회, 업데이트, 삭제의 다섯 가지 엔드포인트와 카테고리 필터가 있는 작동하는 Node.js 서버를 갖게 됩니다. 또한 Mongoose 같은 ODM에 의존하지 않고 기본 MongoDB 드라이버만으로 카테고리 유효성 검사를 강제하는 방법도 확인합니다.
학습 내용
- Node.js, Express, MongoDB로 전체 CRUD 엔드포인트를 포함한 REST API를 구축하는 방법
- 입력값 검증, 카테고리 강제, ObjectId 값을 안전하게 처리하는 방법
- API를 구조화하고 테스트한 뒤 프런트엔드에 연결하는 방법
이 튜토리얼의 전체 코드는 GitHub에서 확인할 수 있습니다. 클론해서 함께 보면서 진행해도 좋습니다.
사전 준비
시작하기 전에 다음이 필요합니다.
- Node.js 18+ 설치(npm 포함)
- 로컬에서 실행 중인 MongoDB 또는 무료 MongoDB Atlas 클러스터
- Postman 또는 임의의 HTTP 클라이언트
- JavaScript 및 REST 개념에 대한 기본 지식
1단계: 프로젝트 설정
이제 간단한 작업 관리자를 만들어 보겠습니다. 먼저 새 폴더를 만들고 아래 명령을 터미널에서 실행해 Node 프로젝트를 초기화합니다.
mkdir task-manager-categories
cd task-manager-categories
npm init -y
다음으로 앱 실행에 필요한 런타임 의존성 네 가지를 설치합니다. 라우팅을 위한 express, 공식 MongoDB 드라이버, 환경 변수 로드를 위한 dotenv, 마지막으로 보안 헤더를 위한 helmet입니다.
npm install express mongodb dotenv helmet
개발용으로는 파일이 변경될 때 서버가 자동으로 재로드되도록 아래 명령으로 nodemon을 dev 의존성으로 설치하세요.
npm install --save-dev nodemon
다음으로 IDE에서 프로젝트 폴더를 열고 package.json의 scripts 섹션을 아래와 같이 업데이트하세요. 이렇게 하면 npm start로 서버를 시작하거나 npm run dev로 개발 모드로 실행할 수 있습니다.
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
이제 진행하면서 만들 구조입니다. 각 폴더의 역할은 명확합니다. db/는 데이터베이스 연결을 처리하고, lib/는 헬퍼 함수를 포함하며, middleware/는 Express 미들웨어를, routes/는 요청 핸들러를 정의합니다. public/ 폴더에는 나중에 추가할 작은 프런트엔드가 들어갑니다. 작은 프로젝트에서도 이 구조는 흐름을 따라가기 쉽고 새 코드를 어디에 넣어야 할지 명확하게 해 줍니다. 지금 폴더 레이아웃을 만들어 두고 진행하며 채워도 되고, 이 단계를 건너뛰고 단계별로 파일을 추가해도 됩니다.
task-manager-categories/
├── db/
│ └── connect.js
├── lib/
│ └── taskDocument.js
├── middleware/
│ └── parseObjectId.js
├── routes/
│ └── tasks.js
├── public/
│ ├── index.html
│ ├── styles.css
│ └── app.js
├── .env
├── .env.example
├── .gitignore
├── package.json
└── server.js
2단계: MongoDB에 연결
작업을 저장하고 조회하려면 먼저 앱을 MongoDB에 연결해야 합니다. 이는 연결 문자열을 사용해 이루어지며, 드라이버에 데이터베이스에 연결하는 방법을 알려 줍니다. 코드를 하드코딩하는 대신 환경 파일에 저장하는 것이 좋습니다. 민감한 값을 코드베이스에서 분리하고, 환경 전환도 쉬워집니다.
필요한 변수를 문서화할 .env.example 파일과 실제 값을 담을 .env 파일을 다음과 같이 만드세요.
# Copy this file to `.env` and fill in real values.
# --- Local MongoDB ---
# Use this if you're running MongoDB locally
MONGO_URI=mongodb://127.0.0.1:27017
# --- MongoDB Atlas ---
# Replace <username>, <password>, and <cluster-url> with your actual values
# Example: mongodb+srv://user:pass@cluster0.abcde.mongodb.net/
# MONGO_URI=mongodb+srv://<username>:<password>@<cluster-url>/?retryWrites=true&w=majority
DB_NAME=taskmanager
PORT=3000
이제 연결 문자열이 준비되었으니 이를 사용해 MongoDB 연결을 설정할 수 있습니다. db/connect.js 파일을 만드세요. 여기에서 MongoDB 클라이언트를 초기화하고 앱의 다른 부분에서 사용할 수 있도록 합니다.
const { MongoClient } = require("mongodb");
let client;
let db;
async function connectDB() {
if (!process.env.MONGO_URI) {
throw new Error("MONGO_URI is not set. Check your .env file.");
}
client = new MongoClient(process.env.MONGO_URI, {
appName: "devrel-tutorial-javascript-crud-geeksforgeeks",
});
await client.connect();
db = client.db(process.env.DB_NAME || "taskmanager");
await db.collection("tasks").createIndex({ category: 1 });
console.log(`MongoDB connected (db: ${db.databaseName})`);
}
function getTasksCollection() {
if (!db) {
throw new Error("Database not initialized. Call connectDB() first.");
}
return db.collection("tasks");
}
async function closeDB() {
if (client) await client.close();
}
module.exports = { connectDB, getTasksCollection, closeDB };
이 파일은 앱 전반에서 재사용할 수 있는 단일 MongoDB 클라이언트를 설정합니다. 드라이버가 내부적으로 커넥션 풀링을 처리하므로 한 번만 생성하는 것이 좋습니다. 요청마다 새 클라이언트를 만들면 처음엔 괜찮아 보일 수 있지만 곧 성능 문제가 발생합니다.
3단계: 작업 구조와 검증 정의
이제 앱이 MongoDB에 연결할 수 있습니다. 저장을 시작하기 전에 작업이 실제로 어떻게 생겼는지 정의해야 합니다.
여기서 기본 MongoDB 드라이버를 사용하면 다소 다르게 느껴지기 시작합니다. Mongoose처럼 스키마 파일이 없습니다. 대신 “스키마”는 데이터베이스에 삽입하는 객체의 모양 자체입니다. 처음에는 느슨해 보일 수 있지만, 실제로는 MongoDB가 실제로 하는 일과 가깝게 유지되며, 추상화 뒤에 숨겨진 것이 없다는 점에서 도움이 됩니다.
그렇더라도 검증은 필요합니다. 이 로직을 여러 라우트에 흩뿌리는 대신 한곳에 모아 모두가 동일한 규칙을 따르도록 하겠습니다. lib/taskDocument.js라는 파일을 만들고 아래 코드를 추가하세요.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
const ALLOWED_CATEGORIES = ['Work', 'Personal', 'Study', 'Other'];
function assertValidCategory(category) {
if (!ALLOWED_CATEGORIES.includes(category)) {
throw new ValidationError(
`category must be one of: ${ALLOWED_CATEGORIES.join(', ')}`
);
}
}
function buildTaskDocument(body = {}) {
if (!body.title || typeof body.title !== 'string' || !body.title.trim()) {
throw new ValidationError('title is required and must be a non-empty string');
}
if (body.category != null) {
assertValidCategory(body.category);
}
const now = new Date();
return {
title: body.title.trim(),
description: typeof body.description === 'string' ? body.description.trim() : '',
category: body.category != null ? body.category : 'Other',
completed: Boolean(body.completed),
createdAt: now,
updatedAt: now
};
}
function buildTaskUpdate(body = {}) {
const updates = {};
if (typeof body.title === 'string' && body.title.trim()) {
updates.title = body.title.trim();
}
if (typeof body.description === 'string') {
updates.description = body.description.trim();
}
if (body.category != null) {
assertValidCategory(body.category);
updates.category = body.category;
}
if (body.completed != null) {
updates.completed = Boolean(body.completed);
}
if (Object.keys(updates).length === 0) {
throw new ValidationError('no valid fields provided for update');
}
updates.updatedAt = new Date();
return updates;
}
module.exports = {
ALLOWED_CATEGORIES,
buildTaskDocument,
buildTaskUpdate,
ValidationError
};
이 파일은 데이터베이스에 들어가는 모든 것의 관문 역할을 합니다. 생성이나 업데이트 요청은 모두 여기로 지나가며, 한곳에서 규칙을 강제합니다.
작업의 형태를 올바르게 보장하고, 카테고리를 일관되게 유지하며, 나중에 데이터를 조회할 때 예상치 못한 일을 방지합니다. 커스텀 ValidationError는 잘못된 입력과 실제 서버 문제를 깔끔하게 분리할 수 있게 해 주므로, API가 적절하게 응답할 수 있습니다. 이것만 갖추면 나머지 앱은 간단하게 유지됩니다. 각 라우트는 이미 유효성이 보장된 데이터를 받는다는 전제하에 자신의 역할에만 집중하면 됩니다. 다음으로 라우트를 연결하고 MongoDB에 작업을 저장하기 시작하겠습니다.
4단계: ObjectId와 요청 검증 처리
이제 작업의 구조를 알았으니, 이를 참조하는 방법을 다뤄 보겠습니다. 라우트에 :id 매개변수가 포함되면 일반 문자열로 들어옵니다. 하지만 MongoDB는 ObjectId를 기대합니다. 문자열 형식이 잘못되면 드라이버는 그다지 도움이 되지 않는 오류를 던집니다. 이를 각 라우트마다 처리하는 대신 미들웨어로 중앙집중화해 모든 엔드포인트가 동일하게 동작하도록 하겠습니다.
이를 위해 middleware/parseObjectId.js 파일을 만들고 아래 내용을 추가하세요.
const { ObjectId } = require('mongodb');
function parseObjectId(req, res, next) {
const { id } = req.params;
if (!ObjectId.isValid(id)) {
return res.status(400).json({ error: 'invalid task id' });
}
req.taskId = new ObjectId(id);
next();
}
module.exports = parseObjectId;
이 미들웨어는 라우트 핸들러 전에 실행됩니다. id의 유효성을 확인하고, 이를 ObjectId로 변환해 req.taskId에 할당합니다. 라우트 로직이 실행될 때는 항상 올바른 ObjectId와 함께 작업하게 되고, 잘못된 입력은 명확한 400 응답으로 일찍 거부됩니다. 이제 이것을 라우트에 연결해 모든 것을 엮어 보겠습니다.
5단계: 작업 라우트 만들기(CRUD + 필터링)
이 시점에서 대부분의 큰 작업은 완료되었습니다. 유효한 작업의 형태를 정의했고, ID 파싱과 검증도 처리했습니다. 이제 라우트 핸들러는 데이터베이스와의 통신에만 집중하면 됩니다.
이제 실제 API 라우트를 만들겠습니다. routes/tasks.js 파일을 만들고 아래 내용을 추가하세요.
const express = require('express');
const { getTasksCollection } = require('../db/connect');
const {
ALLOWED_CATEGORIES,
buildTaskDocument,
buildTaskUpdate,
ValidationError
} = require('../lib/taskDocument');
const parseObjectId = require('../middleware/parseObjectId');
const router = express.Router();
// POST /tasks
router.post('/', async (req, res, next) => {
try {
const doc = buildTaskDocument(req.body);
const result = await getTasksCollection().insertOne(doc);
res.status(201).json({ _id: result.insertedId, ...doc });
} catch (err) {
if (err instanceof ValidationError) {
return res.status(400).json({ error: err.message });
}
next(err);
}
});
// GET /tasks (optionally ?category=Work)
router.get('/', async (req, res, next) => {
try {
const { category } = req.query;
if (category && !ALLOWED_CATEGORIES.includes(category)) {
return res.status(400).json({
error: `category must be one of: ${ALLOWED_CATEGORIES.join(', ')}`
});
}
const filter = category ? { category } : {};
const tasks = await getTasksCollection()
.find(filter)
.sort({ createdAt: -1 })
.toArray();
res.json(tasks);
} catch (err) { next(err); }
});
// GET /tasks/:id
router.get('/:id', parseObjectId, async (req, res, next) => {
try {
const task = await getTasksCollection().findOne({ _id: req.taskId });
if (!task) return res.status(404).json({ error: 'task not found' });
res.json(task);
} catch (err) { next(err); }
});
// PUT /tasks/:id
router.put('/:id', parseObjectId, async (req, res, next) => {
try {
const updates = buildTaskUpdate(req.body);
const result = await getTasksCollection().findOneAndUpdate(
{ _id: req.taskId },
{ $set: updates },
{ returnDocument: 'after' }
);
if (!result) return res.status(404).json({ error: 'task not found' });
res.json(result);
} catch (err) {
if (err instanceof ValidationError) {
return res.status(400).json({ error: err.message });
}
next(err);
}
});
// DELETE /tasks/:id
router.delete('/:id', parseObjectId, async (req, res, next) => {
try {
const result = await getTasksCollection().deleteOne({ _id: req.taskId });
if (result.deletedCount === 0) {
return res.status(404).json({ error: 'task not found' });
}
res.status(204).end();
} catch (err) { next(err); }
});
module.exports = router;
위 코드의 각 라우트는 요청에서 입력을 받고, 앞서 만든 헬퍼를 통과시킨 뒤 MongoDB를 호출하고, 응답을 반환하는 동일한 패턴을 따릅니다. 검증과 ID 파싱이 이미 처리되었기 때문에 여기 코드는 작고 예측 가능하게 유지됩니다. 실제로는 다음과 같습니다.
- 추가(POST) 라우트는 삽입 전에 buildTaskDocument로 새 작업을 생성합니다
- 조회(GET) 라우트는 선택적으로 카테고리로 필터링하고 최신순으로 정렬해 반환합니다
- 수정(PUT) 라우트는 buildTaskUpdate를 사용해 부분 업데이트를 안전하고 일관되게 처리합니다
- 삭제(DELETE) 라우트는 이미 파싱된 ObjectId로 작업을 제거합니다
또한 이 코드에는 주목할 만한 세부 사항이 있습니다.
- find()는 배열이 아닌 커서를 반환하므로 정렬 후 실제 결과를 얻기 위해 .toArray()를 체이닝합니다
- findOneAndUpdate에 returnDocument: 'after'를 사용하면 업데이트된 문서를 바로 받을 수 있습니다
- 적절한 HTTP 상태 코드를 반환합니다. 생성에는 201, 삭제에는 204, 상황에 따라 400 또는 404를 사용합니다
- 예상치 못한 오류는 next(err)로 전달해 모든 라우트 내부가 아니라 한곳에서 처리합니다
6단계: 서버에서 모든 것을 연결
이제 모든 퍼즐 조각이 준비되었습니다. 검증, 깔끔한 라우트, 작동하는 데이터베이스 연결이 있습니다. 남은 일은 모든 것을 연결하고 실제로 서버를 시작하는 것입니다. 앱의 진입점인 server.js 파일을 만듭니다. 여기에서 모든 것이 하나로 모입니다.
require('dotenv').config();
const path = require('path');
const express = require('express');
const helmet = require('helmet');
const { connectDB, closeDB } = require('./db/connect');
const tasksRouter = require('./routes/tasks');
if (!process.env.MONGO_URI) {
console.error('Fatal: MONGO_URI is not set. Copy .env.example to .env and fill it in.');
process.exit(1);
}
const PORT = Number(process.env.PORT) || 3000;
const app = express();
app.use(helmet());
app.use(express.json({ limit: '100kb' }));
app.use(express.static(path.join(__dirname, 'public')));
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'task-manager-with-categories' });
});
app.use('/tasks', tasksRouter);
app.use((req, res) => {
res.status(404).json({ error: 'not found' });
});
app.use((err, req, res, next) => {
if (err.type === 'entity.too.large') {
return res.status(413).json({ error: 'payload too large' });
}
if (err.type === 'entity.parse.failed') {
return res.status(400).json({ error: 'invalid JSON body' });
}
console.error(err);
res.status(500).json({ error: 'internal server error' });
});
async function start() {
try {
await connectDB();
const server = app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
const shutdown = () => {
console.log('\nShutting down gracefully...');
server.close(async () => {
await closeDB();
process.exit(0);
});
setTimeout(() => process.exit(1), 10_000).unref();
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
}
if (require.main === module) {
start();
}
module.exports = { app, start };
이 파일은 모든 것을 깔끔하게 묶습니다. 실행 전에 환경 변수가 설정되었는지 확인하고, helmet으로 기본적인 보안을 적용하며, 들어오는 JSON 크기를 제한해 실수로 거대한 페이로드를 받지 않도록 합니다. 또한 public/ 폴더를 서빙해 별도의 서버 없이도 같은 앱 내에서 프런트엔드가 동작하도록 합니다.
모든 라우트는 /tasks 아래에 마운트되며, 일치하지 않는 요청은 깔끔한 404를 반환합니다. 오류는 모든 라우트에 반복하지 않고 한곳에서 처리되어 일관성과 유지보수성이 좋아집니다. 서버가 요청을 받기 전에 데이터베이스 연결이 먼저 설정되며, 종료 시에도 연결을 적절히 닫는 등 우아하게 종료됩니다.
이제 npm run dev를 실행해 모든 것이 작동하는지 확인해 보세요. 모든 것이 제대로 연결되었다면 다음과 같은 메시지가 표시됩니다.
MongoDB connected (db: taskmanager)
Server running on http://localhost:3000
이제 모든 것이 연결되어 실행 중입니다! 완전한 API가 준비되었으니 테스트해 보겠습니다.
7단계: API 엔드포인트 테스트
UI를 만들기 전에 각 엔드포인트가 자체적으로 작동하는지 확인하는 것이 좋습니다. 이렇게 하면 문제가 API에서 오는지 프런트엔드에서 오는지 빠르게 파악할 수 있어 디버깅이 훨씬 쉬워집니다. Postman(또는 임의의 HTTP 클라이언트)을 사용해 순서대로 몇 가지 요청을 실행하겠습니다. 각 단계는 이전 단계에서 만든 데이터를 바탕으로 진행됩니다.
작업 생성: 제목, 설명, 카테고리를 담은 POST 요청을 보냅니다. 모든 것이 정상이라면 API는 새 작업과 생성된 _id와 함께 201 Created를 반환합니다.
POST http://localhost:3000/tasks
Content-Type: application/json
{
"title": "Write GeeksForGeeks article",
"description": "First draft by Friday",
"category": "Work"
}
몇 개 더 생성: 같은 POST 요청을 다른 본문으로 실행해 목록과 필터링을 테스트할 데이터를 충분히 만드세요. 이후에는 소규모 데이터셋을 조회할 수 있습니다.
{ "title": "Go for a run", "category": "Personal" }
{ "title": "Read MongoDB docs", "category": "Study" }
{ "title": "Buy groceries" } // category defaults to "Other"
모든 작업 조회: 최신순으로 정렬된 모든 작업을 반환합니다.
GET http://localhost:3000/tasks
카테고리로 필터링: 업무(Work) 작업만 반환합니다. 앞서 추가한 인덱스가 데이터가 증가해도 이 쿼리를 빠르게 유지해 줍니다.
GET http://localhost:3000/tasks?category=Work
작업 업데이트: 방금 생성한 작업 중 하나의 _id를 사용합니다(POST 응답이나 목록 엔드포인트에서 복사할 수 있습니다). 업데이트된 작업을 반환합니다. 변경하려는 필드만 보내면 되므로 부분 업데이트가 간단합니다.
PUT http://localhost:3000/tasks/<paste-task-id-here>
Content-Type: application/json
{ "completed": true }
작업 삭제: 삭제하려는 작업의 _id를 사용합니다. 204 No Content를 반환합니다. 같은 작업을 다시 조회하면 404와 "task not found"를 받게 됩니다.
DELETE http://localhost:3000/tasks/<paste-task-id-here>
이제 모든 라우트가 예상대로 동작함을 확인했습니다. API가 제 역할을 하므로, 이제 그 위에 UI를 구축해 보겠습니다.
8단계: API와 상호작용할 프런트엔드 추가
Postman은 API 검증에 훌륭하지만, 실제 웹사이트에서 어떻게 동작하는지도 보고 싶습니다. 애플리케이션에 프런트엔드 코드를 추가해 보겠습니다.
여기에 큰 HTML과 CSS 파일을 붙여넣는 대신, 해당 코드를 GitHub 리포지토리에 추가해 두었습니다. 이동해 index.html, styles.css, favicon.svg의 내용을 프로젝트 루트의 public/ 폴더에 복사하세요. 저장한 뒤 localhost:3000으로 이동합니다. 그런 다음 아래 코드를 app.js 파일에 붙여넣으세요.
const form = document.getElementById('task-form');
const titleEl = document.getElementById('title');
const descEl = document.getElementById('description');
const catEl = document.getElementById('category');
const errEl = document.getElementById('form-error');
const listEl = document.getElementById('task-list');
const emptyEl = document.getElementById('empty');
const filterEl = document.getElementById('filter');
const countEl = document.getElementById('task-count');
async function api(method, path, body) {
const res = await fetch(path, {
method,
headers: body ? { 'Content-Type': 'application/json' } : {},
body: body ? JSON.stringify(body) : undefined
});
if (res.status === 204) return null;
const data = await res.json();
if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`);
return data;
}
const fetchTasks = (category) =>
api('GET', '/tasks' + (category ? `?category=${encodeURIComponent(category)}` : ''));
const createTask = (body) => api('POST', '/tasks', body);
const updateTask = (id, body) => api('PUT', `/tasks/${id}`, body);
const deleteTask = (id) => api('DELETE', `/tasks/${id}`);
function updateCount(tasks) {
const active = tasks.filter((t) => !t.completed).length;
const total = tasks.length;
if (total === 0) {
countEl.textContent = '';
return;
}
const scope = filterEl.value ? ` in ${filterEl.value}` : '';
countEl.textContent = `${total} tasks${scope} · ${active} active`;
}
function renderTask(task) {
const li = document.createElement('li');
li.className = 'task' + (task.completed ? ' completed' : '');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = task.completed;
checkbox.addEventListener('change', () =>
updateTask(task._id, { completed: checkbox.checked }).then(refresh)
);
const title = document.createElement('div');
title.textContent = task.title;
const badge = document.createElement('span');
badge.textContent = task.category;
const del = document.createElement('button');
del.textContent = 'Delete';
del.onclick = () => deleteTask(task._id).then(refresh);
li.append(checkbox, title, badge, del);
return li;
}
function render(tasks) {
listEl.innerHTML = '';
emptyEl.classList.toggle('hidden', tasks.length > 0);
for (const task of tasks) {
listEl.appendChild(renderTask(task));
}
updateCount(tasks);
}
async function refresh() {
try {
const tasks = await fetchTasks(filterEl.value);
render(tasks);
} catch (e) {
errEl.textContent = e.message;
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
errEl.textContent = '';
const title = titleEl.value.trim();
if (!title) {
errEl.textContent = 'Title is required.';
return;
}
try {
await createTask({
title,
description: descEl.value.trim(),
category: catEl.value
});
form.reset();
await refresh();
} catch (e) {
errEl.textContent = e.message;
}
});
filterEl.addEventListener('change', refresh);
refresh();
JavaScript 코드는 앞서 구축한 API로 다시 연결되는 부분입니다. 각 함수는 라우트 하나에 매핑되어 프런트엔드를 이해하기 쉽도록 합니다. 모든 요청은 단일 api() 헬퍼를 통해 가므로, 같은 로직을 반복하지 않고 한곳에서만 오류를 처리합니다.
필터링은 ?category=Work 같은 쿼리 문자열을 전달해 동작하며, 백엔드 로직과 직접 연결됩니다. 작업을 생성, 업데이트, 삭제한 뒤에는 최신 목록을 다시 가져와 UI와 동기화합니다. 작업 카운터는 작은 디테일이지만, 사용하는 동안 앱이 더 생동감 있게 느껴지도록 도와줍니다.
이제 npm run dev로 서버를 다시 시작한 다음, http://localhost:3000을 열어 작업 관리자를 사용해 보세요. 작업을 몇 개 추가하고, 업데이트하고, 카테고리를 전환하고, 일부는 삭제해 보세요. 이제 프런트엔드와 백엔드가 끝까지 완전히 연결되어 함께 작동합니다.
요약
축하합니다! Node.js, Express, MongoDB를 사용해 처음부터 완전한 작업 관리자 API를 성공적으로 구축했습니다. 검증을 처리하고, 라우트를 작게 유지했으며, 모든 것을 작동하는 시스템으로 연결했습니다. 무엇보다 추상화 뒤에 숨기지 않고 MongoDB가 실제로 동작하는 방식에 가깝게 유지했습니다. 이제 여기서부터 프로덕션 준비가 되도록 앱을 발전시켜 나갈 수 있습니다.
핵심 정리
- 미들웨어 사용과 검증의 중앙집중화는 코드를 깔끔하고 예측 가능하게 유지합니다.
- 기본 MongoDB 드라이버는 불필요한 추상화 없이 제어권을 제공합니다.
- 잘 구조화된 API는 프로덕션 준비 앱으로 확장하기 쉽습니다.
FAQs
MongoDB API를 만들려면 Mongoose가 꼭 필요한가요?
아니요. 이 튜토리얼은 네이티브 MongoDB 드라이버를 사용합니다. 더 많은 제어권을 제공하고 가볍습니다. 스키마 추상화가 필요하다면 나중에 Mongoose를 추가할 수 있습니다.
스키마 없이 데이터를 어떻게 검증하나요?
(예: buildTaskDocument) 같은 헬퍼 함수에 검증을 중앙집중화하면, 데이터베이스에 쓰기 전에 모든 라우트가 동일한 규칙을 적용하도록 보장할 수 있습니다.
유효하지 않은 ObjectId를 전달하면 어떻게 되나요?
미들웨어가 이를 조기에 거부하고 400 응답을 반환하여, MongoDB가 혼란스러운 오류를 던지는 일을 방지합니다.