백엔드의 정의
🖥️ 프론트엔드 (Frontend)
→ 사용자가 보는 화면 (HTML, CSS, JavaScript)
→ 예: 버튼, 폼, 이미지, 텍스트 등
⚙️ 백엔드 (Backend)
→ 눈에 보이지 않는 뒷단에서 데이터를 처리
→ 예: 로그인 처리, 데이터 저장, 계산 등

REST API (Application Programming Interface)
API는 프론트엔드와 백엔드가 대화하는 창구이다.
REST API는
- 리소스(데이터)를 URL로 표현하고
- 행동은 HTTP 메서드(동사)로 표현한다
=> endpoint는 웬만해서는 명사(데이터)로
예시: 구독(subscription) 데이터를 다룰 때
GET /api/subscriptions → 전체 목록 조회
GET /api/subscriptions/1 → ID가 1인 항목 조회
POST /api/subscriptions → 새 항목 생성
PATCH /api/subscriptions/1 → ID가 1인 항목 수정
DELETE /api/subscriptions/1 → ID가 1인 항목 삭제
Express
Node.js: JS 실행기 -> JS를 브라우저 밖에서 실행 가능하게 해주는 환경 (엔진)
Express: Node.js위에서 서버를 쉽게 만들게 해주는 프레임워크
Express 없이도 서버를 만들 수 있지만,
Express를 쓰면 훨씬 쉽고 빠르게 만들 수 있다
# 1. 프로젝트 폴더 생성
mkdir my-api
cd my-api
# 2. package.json 생성 (-y: 질문 없이 기본값으로)
npm init -y
# 3. Express 설치
npm install express
# 4. nodemon 설치 (파일 저장할 때마다 서버 자동 재시작)
npm install --save-dev nodemon
{
"name": "my-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
import express from 'express';
// 1. Express 앱 만들기
const app = express();
// 2. 포트 번호 설정 (서버가 열릴 문 번호)
const PORT = 3000;
// 3. 기본 라우트 (루트 경로 접속 시)
app.get('/', (req, res) => {
res.send('서버가 잘 동작하고 있어요! 🎉');
});
// 4. 서버 시작
app.listen(PORT, () => {
console.log(`서버가 http://localhost:${PORT} 에서 실행 중이에요!`);
});
Route
URL경로와 HTTP 메서드의 조합
"GET /api/subscriptions → 구독 목록 보여주는 함수"
"POST /api/subscriptions → 구독 추가하는 함수"
HTTP 메서드가 달라도 서로 다른 경로이다.
(GET / POST / PUT / PATCH / DELETE)
// 라우트의 기본 형태
app.메서드(경로, 처리함수);
app.get('/api/subscriptions', (req, res) => {
// 이 경로로 GET 요청이 오면 실행되는 코드
});
JSON으로 응답하기
JSON은 데이터를 표현하는 형식 중 하나이다.
// res.send() → 문자열 그대로 보냄
app.get('/hello', (req, res) => {
res.send('안녕하세요'); // Content-Type: text/html
});
// res.json() → JSON 형식으로 보냄 (주로 이걸 씁니다!)
app.get('/api/user', (req, res) => {
res.json({ id: 1, name: '홍길동' }); // Content-Type: application/json
});
상태코드
응답에는 항상 상태코드가 함께온다.


API 테스트 도구 (REST Client)
API 엔드포인트 하나 만들때마다 잘 동작하는지 확인해줘야한다.
REST Clinet (Huachao Mao가 만듦)을 사용하도록 하겠


프로젝트에 .http파일을 생성하고 안에 요청 내용을 성한다.
Send Request 버튼을 클릭하면 오른쪽에 응답 결과가 보인다
주의점:
- 변수는 @varName = value 방식으로 정의한다
- Header내용과 data사이에 한줄 띄워줘야 돌아간ㄷ
- 돌리고 싶은 부분을 항이라이트하고 cntrl + alt + r을 해준다
### 파일 상단에 변수 명
@baseUrl = https://panda-market-api.vercel.app
GET {{baseUrl}}/articles?pageSize=10&orderBy=recent
POST {{baseUrl}}/articles
Content-Type: application/json
{
"image": "https://example.com/...",
"content": "게시글 내용입니다.",
"title": "게시글 제목입니다."
}
PATCH {{baseUrl}}/articles/1
Content-Type: application/json
{
"image": "https://example.com/...",
"content": "게시글 내용입니다.",
"title": "게시글 제목입니다."
}
DELETE {{baseUrl}}/articles/1
사용방법
1. http 확장자의 파일을 작성한다 (HTTPMETHOD url)
2. 서버를 킨다
3. cntrl + alt + r을 누른다
CRUD API 만들기
GET 요청
인메모리 데이터로 get 요청을 해보도록 하겠다
1) 기본적인 구조
import express from 'express';
const app = express();
// 임시 데이터 (나중에 DB로 교체할 거예요)
const subscriptions = [
{ id: 1, service: 'Netflix', price: 9900, cycle: 'monthly' },
{ id: 2, service: 'YouTube Premium', price: 14900, cycle: 'monthly' },
{ id: 3, service: 'Spotify', price: 10900, cycle: 'monthly' },
];
// 전체 목록 조회
app.get('/subscriptions', (req, res) => {
res.json(subscriptions);
});
app.listen(3000, () => console.log('서버 실행 중!'));
2) URL 파라미터로 특정 항목 조회하기
req.params.paramName 방식으로 URL 파라미터를 꺼내낸다.
앞에 endpoint에 있는 :id를 보고 req.params.id가 인식되는 것이다.
app.get('/subscriptions/:id', (req, res) => {
// req.params.id 로 URL에서 id 값을 가져옴
const id = Number(req.params.id); // 문자열로 오니까 숫자로 변환!
//아니면 const {param1, param2, param3} = req.params식으로 하는게 깔끔할수 있다
const subscription = subscriptions.find(sub => sub.id === id);
if (!subscription) {
// 못 찾으면 404 에러
return res.status(404).json({ message: `ID ${id}인 구독을 찾을 수 없어요.` });
}
res.json(subscription);
});
3) URL QueryString으로 필터링하기
req.query.queryName방식으로 URL QueryString 값을 꺼낸다.
app.get('/subscriptions', (req, res) => {
// req.query 로 쿼리스트링 값을 가져옴
const { cycle, minPrice } = req.query;
let results = subscriptions;
// cycle 파라미터가 있으면 필터링
if (cycle) {
results = results.filter(sub => sub.cycle === cycle);
}
// minPrice 파라미터가 있으면 필터링
if (minPrice) {
results = results.filter(sub => sub.price >= Number(minPrice));
}
res.json({
count: results.length,
data: results
});
});
이때 응답을 줄때 res.send()모다는 res.json()하는 것이 좋다
sementic하기 때문에 실무 컨벤션으로 잡혀 있다.
항상 Content-Type:application/json으로 헤더를 지정해놓을 수 있어서 편하다
re.send는 전달하는 값의 타입에 따라 Content-Type이 달라져서 문제가 생긴다


POST 요청
POST 요청으로 데이터를 보낼때에는
URL이 아닌 요청 본문 (Body)에 데이터를 담아서 보낸다.
GET → URL에 정보 표시 (누구나 볼 수 있음)
POST → Body에 정보 숨김 (보안상 더 안전)
1) 필요 미들웨어
Express는 Body를 자동으로 파싱해주지 않기 때문에 express.json() 미들웨어를 꼭 추가해야한다.
⭐app.use(express.json())을 꼭 추가해야지 req.body를 쓸 수 있다.
import express from 'express';
import { nanoid } from 'nanoid';
const app = express();
// ✅ 이 줄을 꼭 추가해야 req.body를 쓸 수 있어요!
// req.body = payload;
app.use(express.json());
let subscriptions = [
{ id: nanoid(), service: 'Netflix', price: 9900, cycle: 'monthly' },
];
app.post('/subscriptions', (req, res) => {
const { service, price, cycle } = req.body;
const newSubscription = {
id: nanoid(),
service,
price,
cycle,
};
subscriptions.push(newSubscription);
res.status(201).json(newSubscription);
});
(+) 데이터 추가하기 - 아직 DB 작성안해서 데이터를 직접 만들어야는데
id를 무엇으로 생성해줘야할까 -> nanoid
💡 ID를 어떻게 만들까?
숫자를 직접 계산해서 만들 수도 있지만, nanoid 라이브러리를 쓰면 한 줄로 끝나요!
nanoid가 만들어주는 ID 예시: "V1StGXR8", "abc123xy"
→ 짧고, 겹칠 일 없고, 실무에서도 실제로 쓰여요.
2) body 검증하기
- 클라이언트가 필수 데이터를 빠뜨리고 보낼 수도 있다
- 그래서 저장하기 전에 데이터가 올바른지 먼저 확인해야한다.
app.post('/subscriptions', (req, res) => {
const { service, price, cycle } = req.body;
// ✅ 필수 필드 확인
if (!service || !price || !cycle) {
return res.status(400).json({
message: 'service, price, cycle은 필수 항목이에요.',
});
}
// ✅ 가격이 숫자인지 확인
if (typeof price !== 'number' || price <= 0) {
return res.status(400).json({
message: '가격은 0보다 큰 숫자여야 해요.',
});
}
// ✅ 구독 주기 값 확인
const validCycles = ['daily', 'weekly', 'monthly', 'yearly'];
if (!validCycles.includes(cycle)) {
return res.status(400).json({
message: `구독 주기는 ${validCycles.join(', ')} 중 하나여야 해요.`,
});
}
// 검증 통과 → 저장
const newId = subscriptions.length > 0
? Math.max(...subscriptions.map(s => s.id)) + 1
: 1;
const newSubscription = { id: newId, service, price, cycle };
subscriptions.push(newSubscription);
res.status(201).json(newSubscription);
});
PATCH랑 DELETE
app.patch('/subscriptions/:id', (req, res) => {
const { id } = req.params; // nanoid는 문자열이라 변환 필요 없어요!
const index = subscriptions.findIndex(sub => sub.id === id);
if (index === -1) {
return res.status(404).json({ message: '구독을 찾을 수 없어요.' });
}
subscriptions[index] = {
...subscriptions[index],
...req.body,
};
res.json(subscriptions[index]);
});
app.delete('/subscriptions/:id', (req, res) => {
const { id } = req.params;
const index = subscriptions.findIndex(sub => sub.id === id);
if (index === -1) {
return res.status(404).json({ message: '구독을 찾을 수 없어요.' });
}
const deleted = subscriptions.splice(index, 1)[0];
res.json({ message: '삭제되었어요.', data: deleted });
});
DB 사용하기
데이터를 JS 파일 (메모리)안에 저장하면 문제가 생긴다.
❌ 서버를 재시작하면 데이터가 사라져요
❌ 서버가 여러 대면 데이터가 서로 달라요
❌ 데이터가 많아지면 검색이 느려져요
이를 해결하는 방법이 바로 데이터베이스를 사용하는 것이다.
MongoDB
문서(Document)기반 NoSQL 데이터베이스
- 데이터를 JSON과 비슷한 형태로 저장
- 스키마(테이블구조)가 유연해서 필드 추가가 쉬움)
- JS랑 잘 어울림 (JSON형태 그대로 저장)


(+) model: 특정 collection 내에 CRUD를 가능케하는 인터페이스 객체 (콜렉션을 추상적으로 가리키는 객체)


Username: quothraven1122
Password: 1122
Connection String: mongodb+srv://quothraven1122:1122@cluster0.vf4m04h.mongodb.net/?appName=Cluster0
Mongoose로 MongoseDB 연결하기
Mongoose는 Node.js에서 MongoDB를 편하게 쓰게 해주는 라이브러리이다.
MongoDB를 직접 사용하면:
- 복잡한 쿼리를 직접 작성해야 함
- 데이터 형식을 직접 검증해야 함
- 코드가 복잡해짐
Mongoose를 쓰면:
✅스키마로 데이터 구조를 정의할 수 있음
✅간단한 메서드로 CRUD 가능 (create, find, findById...)
✅자동 유효성 검사
1. npm install mongoose dotevn
2. 코드로 환경 세팅


환경변수로 안전하게 관리하기
1) .env 파일을 생성한다
MONGODB_URI=mongodb+srv://사용자이름:비밀번호@cluster0.xxxxx.mongodb.net/my-api
PORT=3000
2) env 파일을 .gitignore한다
node_modules/
.env
3) app.js에서 dotenv.config()를 호출한다.
import dotenv from 'dotenv';
dotenv.config(); // .env 파일 로드 (맨 먼저!)
4) .env 파일을 process.env.ENVVAR 형태로 꺼낸다
스키마와 모델 만들기
스키마: 데이터 설계도
테이블 속성들의 타입과 유효성 검사를 지정해준다
(서비스는 3중 유효성 검사 - Broswer, API, DB)
예: 구독 데이터 스키마
- service: 문자열, 필수
- price: 숫자, 필수, 0 이상
- cycle: 문자열, monthly/yearly/weekly/daily 중 하나
- startDate: 날짜
이 설계도에 맞지 않는 데이터는 저장 거부! → 데이터 일관성을 유지할 수 있어요.
- required: 필수일때 추가
- trim: 자동으로 앞뒤 공백 제거
- min: 최소 값 지정
- enum: 도메인 설정 (이것중에 하나 골라)
- default: 디폴트값 지정
import mongoose from 'mongoose';
// 1. 스키마 정의 (데이터 구조 설계도)
const subscriptionSchema = new mongoose.Schema(
{
service: {
type: String,
required: [true, '서비스 이름은 필수예요.'],
trim: true, // 앞뒤 공백 자동 제거
},
price: {
type: Number,
required: [true, '가격은 필수예요.'],
min: [0, '가격은 0 이상이어야 해요.'],
},
cycle: {
type: String,
required: [true, '구독 주기는 필수예요.'],
enum: {
values: ['daily', 'weekly', 'monthly', 'yearly'],
message: '올바른 구독 주기가 아니에요.',
},
},
startDate: {
type: Date,
default: Date.now, // 기본값: 현재 날짜
},
},
{
timestamps: true, // createdAt, updatedAt 자동 생성
}
);
// 2. 모델 생성 (실제 DB와 연결되는 객체)
const Subscription = mongoose.model('Subscription', subscriptionSchema);
export default Subscription;
시드 데이터 넣기
시드 데이터: 개발/ 테스트용으로 DB에 미리 넣어두는 샘플 데이터
왜 필요한가요?
- DB를 처음 연결하면 데이터가 텅 비어있어요
- GET 요청을 테스트하려면 조회할 데이터가 있어야 해요
- 매번 POST로 하나씩 추가하기엔 번거로워요
→ 시드 스크립트를 한 번 실행하면 샘플 데이터가 한꺼번에 들어가요!
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import Subscription from './models/Subscription.js';
dotenv.config();
const seedData = [
{ service: 'Netflix', price: 9900, cycle: 'monthly' },
{ service: 'YouTube Premium', price: 14900, cycle: 'monthly' },
{ service: 'Spotify', price: 10900, cycle: 'monthly' },
{ service: 'iCloud 50GB', price: 1100, cycle: 'monthly' },
{ service: 'Adobe CC', price: 61600, cycle: 'yearly' },
];
async function seed() {
await mongoose.connect(process.env.MONGODB_URI);
console.log('✅ DB 연결 성공');
// 기존 데이터 전체 삭제 후 새로 삽입
await Subscription.deleteMany({});
console.log('🗑️ 기존 데이터 삭제 완료');
await Subscription.insertMany(seedData);
console.log(`🌱 시드 데이터 ${seedData.length}개 삽입 완료`);
await mongoose.disconnect();
console.log('👋 DB 연결 종료');
}
seed();
이러고 package.json에 seed라는 스크립트 추가하면 좋다
node see.js를 실행하면 바로 시드 데이터가 설정된다
CRUD 데이터 다루기
1. Create - 데이터 생성
import Subscription from '../models/Subscription.js';
const newSub = await Subscription.create({
service: 'Netflix',
price: 9900,
cycle: 'monthly',
});
Controller: req, res를 받아서 처리하는 함수
export const createSubscription = async (req, res) => {
try {
const subscription = await Subscription.create(req.body);
res.status(201).json(subscription);
} catch (error) {
// Mongoose 유효성 검사 실패 시
res.status(400).json({ message: error.message });
}
};
2. Read - 데이터 조회
// 전체 조회
const subscriptions = await Subscription.find();
// 조건으로 조회
const monthlySubs = await Subscription.find({ cycle: 'monthly' });
// 특정 ID로 조회
const subscription = await Subscription.findById(id);
// 정렬 + 제한
const sorted = await Subscription.find().sort({ price: -1 }).limit(10);
// ↑ -1: 내림차순, 1: 오름차순
// 전체 목록 조회
export const getSubscriptions = async (req, res) => {
try {
const subscriptions = await Subscription.find();
res.json(subscriptions);
} catch (error) {
res.status(500).json({ message: error.message });
}
};
// 특정 항목 조회
export const getSubscriptionById = async (req, res) => {
try {
const subscription = await Subscription.findById(req.params.id);
if (!subscription) {
return res.status(404).json({ message: '구독을 찾을 수 없어요.' });
}
res.json(subscription);
} catch (error) {
// 잘못된 ID 형식일 때 (예: 숫자로 요청)
res.status(400).json({ message: '유효하지 않은 ID 형식이에요.' });
}
};
3. Update 데이터 수정
const updated = await Subscription.findByIdAndUpdate(
id, // 수정할 문서의 ID
req.body, // 수정할 내용
{
new: true, // 수정된 문서를 반환 (기본은 수정 전 문서)
runValidators: true // 스키마 유효성 검사 실행
}
);
export const updateSubscription = async (req, res) => {
try {
const subscription = await Subscription.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!subscription) {
return res.status(404).json({ message: '구독을 찾을 수 없어요.' });
}
res.json(subscription);
} catch (error) {
res.status(400).json({ message: error.message });
}
};
4. delete - 데이터 삭제
const deleted = await Subscription.findByIdAndDelete(id);
컨트롤러 적용 예시
export const deleteSubscription = async (req, res) => {
try {
const subscription = await Subscription.findByIdAndDelete(req.params.id);
if (!subscription) {
return res.status(404).json({ message: '구독을 찾을 수 없어요.' });
}
// 삭제 성공 → 204 No Content
res.status(204).send();
} catch (error) {
res.status(500).json({ message: error.message });
}
};
비동기 에러 처리 패턴
모든 라우트에 try-catch를 반복 작성하면 코드가 지저분해져요.
→ asyncHandler 안에서 에러를 직접 처리하면 한 번에 해결!
MongoDB에서 자주 발생하는 에러 두 가지:
- ValidationError → 스키마 유효성 검사 실패 (예: 필수 필드 누락)
→ 400 Bad Request
- CastError → 잘못된 ID 형식으로 조회 시 발생
→ 404 Not Found
- 그 외 → 예상치 못한 서버 오류
→ 500 Internal Server Error
asyncHandler.js
export const asyncHandler = (fn) => {
return async (req, res) => {
try {
await fn(req, res);
} catch (e) {
if (e.name === 'ValidationError') {
// 스키마 유효성 검사 실패 (필수 필드 누락, enum 불일치 등)
res.status(400).send({ message: e.message });
} else if (e.name === 'CastError') {
// 올바르지 않은 ID 형식으로 조회했을 때
res.status(404).send({ message: 'Cannot find given id.' });
} else {
// 그 외 예상치 못한 서버 오류
res.status(500).send({ message: e.message });
}
}
};
};
import { asyncHandler } from '../utils/asyncHandler.js';
// try-catch 없이 깔끔하게! 에러는 asyncHandler 내부에서 처리됨
export const getTodos = asyncHandler(async (req, res) => {
const todos = await Todo.find();
res.json(todos);
});
배포 전에 CORS 설정하기
1. 브라우저의 기본 규칙 SOP (Same-Origin Policy)
서버는 기본적으로 다른 도메인에서 오는 요청을 막는다.

"같은 출처끼리만 자유롭게 통신해. 다른 출처는 기본적으로 막을게."
모든 브라우저의 기본값입니다. 우리가 켠 적 없는데 원래 켜져 있어요.
출처 = 프로토콜 + 도메인 + 포트

2. CORS (Cross-Origin) - SOP의 예외를 허용하는 규칙
현실에서는 다른 출처랑 통신할 일이 너무 많다.
요즘 웹은 거의다 프론트/백 분리 구조인데, SOP 그대로 두면 아무것도 못한다.
그래서 안전하게 다른 출처와 통신할 수 있게 해주는 규칙이 필요해졌다.
그것이 바로 CORS이다.
CORS = Cross-Origin Resource Sharing
한 줄 정의: "다른 출처여도 서버가 허락하면 통신해도 돼" 라고 알려주는 약속

SOP와 CORS 모두 브라우저에 강제하는 보안 규칙이다.

🚨 CORS 에러는 "서버가 응답을 안 줘서"가 아니라 "브라우저가 받은 응답을 자바스크립트에게 안 넘겨줘서" 발생
즉, 서버 입장에선 요청 받고 처리하고 응답까지 다 보냈어요. 다만 브라우저가 그 응답을 코드에 전달하지 않고 차단하는 것뿐이에요.
💡 그래서 REST Client/Postman으로 테스트할 때는 CORS가 전혀 문제없었던 거예요! CORS가 필요해지는 순간은
실제 프론트엔드 코드에서 브라우저로 fetch()를 호출할 때예요.
3. CORS 세팅 방법
npm install cors
import cors from 'cors';
// 개발할 때 (모든 도메인 허용 - 개발 편의상)
app.use(cors());
// 배포할 때 (특정 도메인만 허용 - 보안상 좋음)
app.use(cors({
origin: [
'http://localhost:5173', // 개발용 프론트엔드
'https://my-app.vercel.app', // 배포된 프론트엔드
],
}));
근데 도메인들도 환경변수로 관리하는것이 좋다
'코드잇 스터디' 카테고리의 다른 글
| [관계형DB] 데이터베이스를 활용한 JS (0) | 2026.05.11 |
|---|---|
| [React] 리액트로 웹사이트 만들기 (0) | 2026.04.28 |
| [React] React로 데이터 다루기 (1) | 2026.04.22 |
| [React] 리엑트에 대하여 (0) | 2026.04.21 |
| [JS 기초문법] 리퀘스트 보내기 (0) | 2026.04.14 |