📘 Node.js + Express + React + Storybook: 유튜버 관리 프로젝트
유튜버 정보를 등록하고, 수정하며, 삭제할 수 있는 풀스택 CRUD 프로젝트입니다.
Express.js + React + Tailwind CSS + Storybook 기반으로 직접 API 설계부터 UI 구현과 컴포넌트 문서화하였습니다.
영역 | 기술 |
---|---|
Backend (Node.js + Express) | - 유튜버 등록, 조회, 수정, 삭제 기능을 포함한 RESTful API 구현 - 개별 유튜버 관리용 API를 직접 설계 및 라우팅 처리 - 데이터는 Map() 객체를 사용하여 간단한 인메모리 DB 구조로 관리 |
Database (db.json) | - JSON 파일 기반 가상 DB |
Frontend (React + Tailwind CSS + Axios) | - React와 Tailwind를 활용해 깔끔하고 모던한 UI 구현 - API 호출은 Axios로 관리하며 GET / POST / PUT / DELETE 요청 수행 - 전체 코드는 간결한 구조로 유지하면서도 확장성 고려 |
UI 문서화 (Storybook) | Button, Header, Input, YoutuberCard 등 공통 UI 컴포넌트 분리 |
/youtubers 경로로 RESTful API 제공
db.json 파일 기반으로 CRUD 작동
readData, writeData, getNextId 유틸 함수로 깔끔하게 관리
GET /youtubers → 유튜버 전체 목록 조회
GET /youtubers/:id → 유튜버 조회 개별 조회
POST /youtubers → 유튜버 등록
PUT /youtubers/:id → 유튜버 수정
DELETE /youtubers/:id → 유튜버 삭제
backend/
├── app.js
├── routes/
│ └── youtubers.js
├── data/
│ └── db.js
└── package.json
const express = require("express");
const fs = require("fs");
const path = require("path");
const router = express.Router();
// 경로 및 유틸 함수 정의
const DB_PATH = path.join(__dirname, "../data/db.json");
// 유틸 함수
const readData = () => {
const raw = fs.readFileSync(DB_PATH, "utf-8");
return JSON.parse(raw).youtubers;
};
// db.json 파일에 저장하는 쓰기 함수
const writeData = (list) => {
fs.writeFileSync(DB_PATH, JSON.stringify({ youtubers: list }, null, 2));
};
// 다음 등록할 유튜버의 id를 자동으로 계산해주는 함수
const getNextId = (list) => {
const ids = list.map((yt) => yt.id);
return ids.length === 0 ? 1 : Math.max(...ids) + 1; // 가장 큰 id 찾아서 +1
};
// 전체 조회
router.get("/", (req, res) => {
const list = readData();
res.json(list);
});
// 개별 조회
router.get("/:id", (req, res) => {
const list = readData();
const numericId = parseInt(req.params.id);
const youtuber = list.find((yt) => yt.id === numericId);
if (!youtuber) {
return res.status(404).json({ message: "없는 youtuber입니다." });
}
res.json(youtuber);
});
// 등록
router.post("/", (req, res) => {
const list = readData(); // db.json에서 유튜버 데이터 불러오기
const newId = getNextId(list); // list 데이터를 매개변수로 받아서 id 계산
const newYoutuber = { id: newId, ...req.body }; // 새 유튜버 객체 생성
list.push(newYoutuber); // 배열에 추가
writeData(list); // 변경된 list를 db.json에 다시 저장
res.json({
message: `${req.body.channelTitle} 유튜버님 생활을 응원합니다!`,
id: newId,
});
});
// 수정
router.put("/:id", (req, res) => {
const list = readData();
const numericId = parseInt(req.params.id);
const index = list.findIndex((yt) => yt.id === numericId);
if (index === -1) {
return res.status(404).json({ message: "없는 youtuber입니다." });
}
list[index] = { id: numericId, ...req.body };
writeData(list);
res.json({ message: "수정 완료", id: numericId });
});
// 삭제
router.delete("/:id", (req, res) => {
const list = readData();
const numericId = parseInt(req.params.id);
const newList = list.filter((yt) => yt.id !== numericId);
if (list.length === newList.length) {
return res.status(404).json({ message: "없는 youtuber입니다." });
}
writeData(newList);
res.json({ message: `id ${numericId} 유튜버 삭제 완료` });
});
module.exports = router;
const express = require("express");
const app = express();
const youtuberRouter = require("./routes/youtubers");
app.use(express.json());
app.use("/youtubers", youtuberRouter);
app.listen(1234, () => {
console.log("🚀 서버 실행: http://localhost:1234");
});
기능 | Method | Endpoint | Body |
---|---|---|---|
전체 조회 | GET | /youtubers |
- |
개별 조회 | GET | /youtubers/1 |
- |
등록 | POST | /youtubers |
{ "channelTitle": "브이로그", "sub": "10만명", "videoNum": "77개" } |
수정 | PUT | /youtubers/2 |
{ "channelTitle": "수정한내용", ... } |
삭제 | DELETE | /youtubers/2 |
- |
Component
→ API
→ Page
구조로 유지복수와 확장성 고려frontend/
├── src/
│ ├── pages/ # 페이지 단위 컴포넌트
│ │ ├── YoutuberList.jsx # 유튜버 전체 목록 페이지
│ │ ├── YoutuberDetail.jsx # 유튜버 상세 및 수정 페이지
│ │ ├── YoutuberForm.jsx # 유튜버 등록 페이지
│ ├── api/ # API 호출 함수 모음
│ │ └── youtuber.js
│ ├── components/ # 공통 UI 컴포넌트 (Button, Header, Input 등)
│ └── App.jsx # 라우팅 및 전역 설정
import axios from "axios";
const BASE_URL = "http://localhost:1234/youtubers";
export const getAllYoutubers = () => axios.get(BASE_URL);
export const getYoutuber = (id) => axios.get(`${BASE_URL}/${id}`);
export const createYoutuber = (data) => axios.post(BASE_URL, data);
export const updateYoutuber = (id, data) => axios.put(`${BASE_URL}/${id}`, data);
export const deleteYoutuber = (id) => axios.delete(`${BASE_URL}/${id}`);
유튜버 목록을 가져오는 API를 호출하고, 상태로 관리하는 기본적인 useEffect 흐름입니다.
import { useEffect, useState } from "react";
import { getAllYoutubers } from "../api/youtuber";
export default function YoutuberList() {
const [list, setList] = useState([]);
useEffect(() => {
getAllYoutubers().then((res) => setList(res.data));
}, []);
return (
<div>
{list.map((yt) => (
<div key={yt.id}>
<h2>{yt.channelTitle}</h2>
<p>{yt.sub}</p>
</div>
))}
</div>
);
}
Button
, Input
, Header
, YoutuberCard
, Loader
등 재사용 가능한 UI 컴포넌트로 구성components/
├── Button.jsx # variant, icon, label props과 함께 가능
├── Input.jsx # placeholder, name, value, onChange
├── Header.jsx # title, showBack, rightElement
├── YoutuberCard.jsx # 정보 + 수정/삭제 Button 포함
└── Loader.jsx # 로딩 UI
Storybook을 활용해 UI 컴포넌트를 독립적으로 개발하고 시각적으로 테스트 및 문서화
.stories.jsx
(예: Button.stories.jsx
)