Chapter 7. Express 미들웨어 & API 응답 통일 & 에러 핸들링
☑️ 실습 인증
express 미들웨어
<초기 설정 후 index.js 코드 작성>
import express from 'express';
const app = express();
const port = 3000;
const myLogger = (req, res, next) => {
console.log("LOGGED");
next();
}
app.use(myLogger);
app.get('/', (req, res) => {
console.log("/");
res.send('Hello UMC!');
});
app.get('/hello', (req, res) => {
console.log("/hello");
res.send('Hello world!');
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
1. /에 접속 시 화면에 /경로의 미들웨어 함수가 실행

2. hello 로 접속

3. myLogger의 next() 를 제거했을 때

⭐알 수 있는 점 ⭐
- next()를 제거하면 myLogger 미들웨어가 호출된 후, /나 /hello 경로에 대한 응답이 클라이언트로 전송되지 않고 중단된다.
- 즉 next()는 미들웨어에서 요청 흐름을 이어가기 위해 필수적이다.
api 응답 통일 & 에러 핸들링
👉 API 응답 통일
src/controllers/user.controller.js 파일 수정
import { StatusCodes } from "http-status-codes";
import { bodyToUser } from "../dtos/user.dto.js";
import { userSignUp, listUserReviews } from "../services/user.service.js";
export const handleUserSignUp = async (req, res, next) => {
try {
const user = await userSignUp(bodyToUser(req.body));
res.status(StatusCodes.OK).success(user);
} catch (error) {
next(error);
}
};
export const handleListUserReviews = async (req, res, next) => {
try {
const userId = parseInt(req.params.userId, 10);
const cursor = req.query.cursor ? parseInt(req.query.cursor, 10) : 0;
const reviews = await listUserReviews(userId, cursor);
res.status(StatusCodes.OK).success(reviews);
} catch (error) {
next(error);
}
};
index.js 파일을 다음과 같이 수정
// src/index.js
import express from "express";
import cors from "cors";
import { handleStoreSignUp,handleListStoreReviews} from "./controller/store.controller.js";
import { handleReviewSignUp } from "./controller/review.controller.js";
import { handleMissionSignUp,handleListStoreMissions,handleListUserInProgressMissions,handleCompleteUserMission } from "./controller/mission.controller.js";
import { handleChallengeSignUp } from "./controller/challenge.controller.js";
import { handleListUserReviews } from './controller/user.controller.js';
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use(cors());
app.use((req, res, next) => {
res.success = (success) => {
return res.json({ resultType: "SUCCESS", error: null, success });
};
res.error = ({ errorCode = "unknown", reason = null, data = null }) => {
return res.json({
resultType: "FAIL",
error: { errorCode, reason, data },
success: null,
});
};
next();
});
// 가게 추가 API
app.post("/api/stores", handleStoreSignUp);
// 가게 리뷰 추가 API
app.post("/api/reviews", handleReviewSignUp);
// 가게 미션 추가 API
app.post("/api/missions", handleMissionSignUp);
// 가게의 미션 도전하기 API
app.post("/api/challenges", handleChallengeSignUp);
// 리뷰 목록 확인
app.get("/api/stores/:storeId/reviews", handleListStoreReviews)
// 내가 작성한 리뷰 목록 확인
app.get('/api/users/:userId/reviews', handleListUserReviews);
// 특정 가게의 미션 목록 조회 API
app.get('/api/stores/:storeId/missions', handleListStoreMissions);
// 특정 사용자의 진행 중인 미션 목록 조회 API
app.get('/api/users/:userId/missions/in-progress', handleListUserInProgressMissions);
// 특정 사용자의 진행 중인 미션 완료로 업데이트 API
app.put('/api/users/:userId/missions/:missionId/complete', handleCompleteUserMission);
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
res.status(err.statusCode || 500).error({
errorCode: err.errorCode || "unknown",
reason: err.reason || err.message || null,
data: err.data || null,
});
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
👉 테스트 해보기
GET <http://localhost:3000/api/users/1/reviews?cursor>
Content-Type: application/json

👉 오류 응답 개선하기
src/error.js 파일 생성
export class DuplicateUserEmailError extends Error {
errorCode = "U001";
constructor(reason, data) {
super(reason);
this.reason = reason;
this.data = data;
}
} // 이메일 중복 생성
🎯핵심 키워드 정리
💠미들웨어란?
- 미들웨어는 요청(Request)과 응답(Response) 사이에서 특정 작업을 수행하는 함수를 말한다.
- 서버로 들어온 요청을 처리하기 전에 실행되어, 인증, 로그 기록, 데이터 파싱, 오류 처리 등을 수행 수 있다.
- Express 같은 웹 프레임워크에서 요청이 들어오면 여러 미들웨어가 순서대로 실행되면서 요청을 처리하고, 마지막에 응답을 반환한다.
💠HTTP 상태코드
HTTP 상태 코드는 서버가 클라이언트의 요청에 대해 응답할 때 보내는 숫자 코드로, 요청 처리 결과나 오류 상태를 나타낸다.
1. 1xx (정보 응답)
- 100 Continue: 요청의 일부를 수신했으며, 계속해서 요청을 이어가라는 의미.
- 101 Switching Protocols: 클라이언트가 요청한 프로토콜로 변경한다는 의미.
2. 2xx (성공)
- 200 OK: 요청이 성공적으로 처리되었음을 나타냅니다. 일반적인 성공 응답.
- 201 Created: 요청이 성공적으로 처리되어 리소스가 생성되었음을 의미. 주로 POST 요청에 사용됨.
- 204 No Content: 요청이 성공적으로 처리되었지만 반환할 콘텐츠가 없음을 의미.
3. 3xx (리다이렉션)
- 301 Moved Permanently: 요청한 리소스가 영구적으로 다른 위치로 이동되었음을 나타냄.
- 302 Found: 요청한 리소스가 일시적으로 다른 위치에 있음을 나타냄.
- 304 Not Modified: 요청한 리소스가 수정되지 않았으므로 클라이언트는 캐시된 버전을 사용할 수 있음을 의미.
4. 4xx (클라이언트 오류)
- 400 Bad Request: 클라이언트의 요청이 잘못되었음을 나타냄.
- 401 Unauthorized: 인증이 필요하지만 제공되지 않았거나 잘못된 인증 정보.
- 403 Forbidden: 서버가 요청을 이해했지만 권한이 없어 거절된 경우.
- 404 Not Found: 요청한 리소스를 찾을 수 없음을 의미.
- 405 Method Not Allowed: 요청된 HTTP 메서드가 허용되지 않음을 의미.
5. 5xx (서버 오류)
- 500 Internal Server Error: 서버에서 오류가 발생하여 요청을 처리할 수 없음을 나타냅니다.
- 501 Not Implemented: 서버가 요청을 처리할 기능을 지원하지 않음을 의미
- 502 Bad Gateway: 게이트웨이 또는 프록시 서버가 잘못된 응답을 받았음을 나타냄
- 503 Service Unavailable: 서버가 과부하 또는 유지 보수 중이라 요청을 처리할 수 없음을 나타냄
- 504 Gateway Timeout: 게이트웨이 또는 프록시 서버가 응답을 기다리다 시간 초과된 경우.
✏️7주차 필기 및 마인드맵
🔥 미션 인증
api 통일 및 error 객체 사용
👉 API 응답 통일
우선 api 통일을 위해 index.js 파일을 수정해주었습니다. (실습을 참고하였습니다)
index.js 파일 수정
// src/index.js
import express from "express";
import cors from "cors";
import { handleStoreSignUp,handleListStoreReviews} from "./controller/store.controller.js";
import { handleReviewSignUp } from "./controller/review.controller.js";
import { handleMissionSignUp,handleListStoreMissions,handleListUserInProgressMissions,handleCompleteUserMission } from "./controller/mission.controller.js";
import { handleChallengeSignUp } from "./controller/challenge.controller.js";
import { handleListUserReviews } from './controller/user.controller.js';
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use(cors());
**app.use((req, res, next) => {
res.success = (success) => {
return res.json({ resultType: "SUCCESS", error: null, success });
};
res.error = ({ errorCode = "unknown", reason = null, data = null }) => {
return res.json({
resultType: "FAIL",
error: { errorCode, reason, data },
success: null,
});
};
next();
});**
// 가게 추가 API
app.post("/api/stores", handleStoreSignUp);
// 가게 리뷰 추가 API
app.post("/api/reviews", handleReviewSignUp);
// 가게 미션 추가 API
app.post("/api/missions", handleMissionSignUp);
// 가게의 미션 도전하기 API
app.post("/api/challenges", handleChallengeSignUp);
// 리뷰 목록 확인
app.get("/api/stores/:storeId/reviews", handleListStoreReviews)
// 내가 작성한 리뷰 목록 확인
app.get('/api/users/:userId/reviews', handleListUserReviews);
// 특정 가게의 미션 목록 조회 API
app.get('/api/stores/:storeId/missions', handleListStoreMissions);
// 특정 사용자의 진행 중인 미션 목록 조회 API
app.get('/api/users/:userId/missions/in-progress', handleListUserInProgressMissions);
// 특정 사용자의 진행 중인 미션 완료로 업데이트 API
app.put('/api/users/:userId/missions/:missionId/complete', handleCompleteUserMission);
**/**
* 전역 오류를 처리하기 위한 미들웨어
*/
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
res.status(err.statusCode || 500).error({
errorCode: err.errorCode || "unknown",
reason: err.reason || err.message || null,
data: err.data || null,
});**
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
이런식으로 요청 성공/실패 후의 api를 통일 시키고 난 다음 controller의 함수들을 수정해주었습니다.
👉 오류 응답 개선하기
src.errors.js 파일 수정
각 오류에 맞는 클래스들을 정의해주었습니다.
<각 클래스 설명>
- DuplicateUserEmailError: 사용자가 회원가입 시 이메일이 이미 존재하는 경우 발생
- MissionNotFoundError: 특정 미션을 찾을 수 없거나, 미션이 이미 완료된 상태일 때 발생
- StoreNotFoundError: 가게를 찾을 수 없을 때 발생하며, 예를 들어 가게가 존재하지 않는 경우에 사용
- ReviewAddError: 리뷰 추가가 실패할 경우 발생
- ChallengeAddError: 챌린지 추가에 실패한 경우 발생
// src/errors.js
export class DuplicateUserEmailError extends Error {
errorCode = "U001";
constructor(reason, data) {
super(reason);
this.reason = reason;
this.data = data;
}
}//사용자 이메일 중복 오류 클래스
export class MissionNotFoundError extends Error {
errorCode = "M001";
constructor(reason, data) {
super(reason);
this.reason = reason;
this.data = data;
}
}//미션 조회 오류 클래스
export class StoreNotFoundError extends Error {
errorCode = "S001";
constructor(reason, data) {
super(reason);
this.reason = reason;
this.data = data;
}
}//가게 조회 오류 클래스
export class ReviewAddError extends Error {
errorCode = "R001";
constructor(reason, data) {
super(reason);
this.reason = reason;
this.data = data;
}
}//리뷰 추가 실패 오류 클래스
export class ChallengeAddError extends Error {
errorCode = "C001";
constructor(reason, data) {
super(reason);
this.reason = reason;
this.data = data;
}
}//챌린지 추가 실패 오류 클래스
모든 파일들 오류 핸들러 함수 수정 ⭐
1️⃣ controller 파일들을 전체적으로 수정해주었습니다 (status,error 부분)
import { StatusCodes } from "http-status-codes";
import { bodyToChallenge } from "../dtos/challenge.dto.js";
import { challengeSignUp } from "../services/challenge.service.js";
export const handleChallengeSignUp = async (req, res, next) => {
try {
const challenge = await challengeSignUp(bodyToChallenge(req.body));
res.status(StatusCodes.CREATED).success(challenge);
} catch (error) {
next(error);
}
};
import { StatusCodes } from "http-status-codes";
import { bodyToChallenge } from "../dtos/challenge.dto.js";
import { challengeSignUp } from "../services/challenge.service.js";
export const handleChallengeSignUp = async (req, res, next) => {
try {
const challenge = await challengeSignUp(bodyToChallenge(req.body));
res.status(StatusCodes.CREATED).success(challenge);
} catch (error) {
next(error);
}
};
import { StatusCodes } from "http-status-codes";
import { bodyToChallenge } from "../dtos/challenge.dto.js";
import { challengeSignUp } from "../services/challenge.service.js";
export const handleChallengeSignUp = async (req, res, next) => {
try {
const challenge = await challengeSignUp(bodyToChallenge(req.body));
res.status(StatusCodes.CREATED).success(challenge);
} catch (error) {
next(error);
}
};
import { StatusCodes } from "http-status-codes";
import { bodyToChallenge } from "../dtos/challenge.dto.js";
import { challengeSignUp } from "../services/challenge.service.js";
export const handleChallengeSignUp = async (req, res, next) => {
try {
const challenge = await challengeSignUp(bodyToChallenge(req.body));
res.status(StatusCodes.CREATED).success(challenge);
} catch (error) {
next(error);
}
};
import { StatusCodes } from "http-status-codes";
import { bodyToChallenge } from "../dtos/challenge.dto.js";
import { challengeSignUp } from "../services/challenge.service.js";
export const handleChallengeSignUp = async (req, res, next) => {
try {
const challenge = await challengeSignUp(bodyToChallenge(req.body));
res.status(StatusCodes.CREATED).success(challenge);
} catch (error) {
next(error);
}
};
2️⃣ service 함수의 에러 핸들러 부분도 같이 수정해주었습니다.
// src/services/challenge.service.js
import { addChallenge } from "../repositories/challenge.repository.js";
import { responseFromChallenge } from "../dtos/challenge.dto.js";
import { ChallengeAddError } from "../error.js";
export const challengeSignUp = async (data) => {
try {
const userMissionId = await addChallenge(data);
return responseFromChallenge({ userMissionId, ...data });
} catch (error) {
**throw new ChallengeAddError("미션 도전에 실패했습니다.", { ...data, reason: error.message });**
}
};
import { MissionNotFoundError } from "../error.js";
import { addMission, getStoreMissions, getUserInProgressMissions, completeUserMission } from "../repositories/mission.repository.js";
import { responseFromMission, responseFromMissions } from "../dtos/mission.dto.js";
export const missionSignUp = async (data) => {
const missionId = await addMission(data);
if (!missionId) {
throw new MissionNotFoundError("미션을 추가할 수 없습니다.", data);
}
return responseFromMission({ missionId, ...data });
};
export const listStoreMissions = async (storeId, cursor) => {
const missions = await getStoreMissions(storeId, cursor);
return responseFromMissions(missions);
};
export const listUserInProgressMissions = async (userId, cursor) => {
const missions = await getUserInProgressMissions(userId, cursor);
return responseFromMissions(missions);
};
export const markMissionAsCompleted = async (userId, missionId) => {
const result = await completeUserMission(userId, missionId);
if (result.count === 0) {
**throw new MissionNotFoundError('해당 미션을 찾을 수 없거나 이미 완료된 상태입니다.', { userId, missionId });**
}
return { message: '미션이 완료되었습니다.' };
};
import { addReview, checkStoreExists } from "../repositories/review.repository.js";
import { responseFromReview } from "../dtos/review.dto.js";
import { StoreNotFoundError, ReviewAddError } from "../error.js";
export const reviewSignUp = async (data) => {
const storeExists = await checkStoreExists(data.storeId);
if (!storeExists) {
**throw new StoreNotFoundError("리뷰를 추가할 가게가 존재하지 않습니다.", { storeId: data.storeId });**
}
const reviewId = await addReview(data);
if (!reviewId) {
**throw new ReviewAddError("리뷰를 추가할 수 없습니다.", data);**
}
return responseFromReview({ reviewId, ...data });
};
// src/services/store.service.js
import { addStore } from "../repositories/store.repository.js";
import { responseFromStore } from "../dtos/store.dto.js";
import { getAllStoreReviews } from '../repositories/user.repository.js';
import {responseFromReviews} from '../dtos/review.dto.js'
export const storeSignUp = async (data) => {
const storeId = await addStore({
storeName: data.storeName,
address: data.address,
regionId: data.regionId,
});
if (!storeId) {
**throw new Error("가게를 추가할 수 없습니다.");**
}
return responseFromStore({ storeId, ...data });
};
export const listStoreReviews = async (storeId, cursor) => {
const reviews = await getAllStoreReviews(storeId, cursor);
console.log("Raw reviews from DB:", reviews); // 리뷰 배열이 제대로 반환되는지 확인
return responseFromReviews(reviews); // 리뷰 배열을 변환하여 반환
};
import { responseFromUser } from "../dtos/user.dto.js";
import { getUserReviews, addUser, getUser, getUserPreferencesByUserId, setPreference } from "../repositories/user.repository.js";
import { responseFromReviews } from '../dtos/review.dto.js';
import { DuplicateUserEmailError } from '../error.js';
export const userSignUp = async (data) => {
const joinUserId = await addUser({
email: data.email,
name: data.name,
gender: data.gender,
birth: data.birth,
address: data.address,
detailAddress: data.detailAddress,
phoneNumber: data.phoneNumber,
});
if (joinUserId === null) {
**throw new DuplicateUserEmailError("이미 존재하는 이메일입니다.", data);**
}
for (const preference of data.preferences) {
await setPreference(joinUserId, preference);
}
const user = await getUser(joinUserId);
const preferences = await getUserPreferencesByUserId(joinUserId);
return responseFromUser({ user, preferences });
};
export const listUserReviews = async (userId, cursor) => {
const reviews = await getUserReviews(userId, cursor);
return responseFromReviews(reviews);
};
api 응답 검토 및 오류 점검
💠특정 지역에 가게 추가
### 특정 지역에 가게 추가
POST <http://localhost:3000/api/stores>
Content-Type: application/json
{
"storeName": "Awesome Cafe",
"address": "123 Coffee Street",
"regionId": 1
}
☑️ 실행결과

💠가게에 리뷰 추가
### 가게에 리뷰 추가
POST <http://localhost:3000/api/reviews>
Content-Type: application/json
{
"userId": 1,
"storeId": 1,
"rating": 5,
"reviewText": "Amazing coffee and cozy atmosphere!"
}
☑️ 실행결과

💠미션 추가
### 미션 추가
POST <http://localhost:3000/api/missions>
Content-Type: application/json
{
"regionId": 1,
"missionStatus": "IN_PROGRESS",
"description": "Discover Seoul's best coffee shops"
}
☑️ 실행결과

💠미션 도전 (가게의 미션을 도전 중인 미션에 추가)
### 미션 도전 (가게의 미션을 도전 중인 미션에 추가)
POST <http://localhost:3000/api/challenges>
Content-Type: application/json
{
"userId": 1,
"missionId": 1,
"storeId": 1,
"status": "IN_PROGRESS"
}
☑️ 실행결과

💠특정 가게의 리뷰 목록 조회
GET <http://localhost:3000/api/stores/1/reviews?cursor>
Content-Type: application/json
☑️ 실행결과

💠내 리뷰 목록 조회
GET <http://localhost:3000/api/users/1/reviews?cursor>
Content-Type: application/json
☑️ 실행결과

💠특정 가게 미션 조회
GET <http://localhost:3000/api/stores/1/missions?cursor>
Content-Type: application/json
☑️ 실행결과

💠내가 진행중인 미션목록 조회
###내가 진행중인 미션목록 조회
GET <http://localhost:3000/api/users/1/missions/in-progress?cursor=0>
Content-Type: application/json
☑️ 실행결과

💠내가 진행 중인 미션을 진행완료로 바꾸기
PUT <http://localhost:3000/api/users/1/missions/1/complete>
Content-Type: application/json
☑️ 실행결과 (오류 핸들러 정상 작동 )


'🏃♀️ 대외활동 > UMC 7기 - Node.js' 카테고리의 다른 글
[UMC 7th Server] 6, 7주차 트러블 슈팅 기록 (0) | 2024.11.19 |
---|---|
[UMC 7th Server] Chapter 8. 프론트엔드 연동과 Swagger (2) | 2024.11.18 |
[UMC 7th Server] Chapter 6. ORM 사용해보기 (5) | 2024.11.07 |
[UMC 7th Server] Chapter 5. ES6와 프로젝트 파일 구조의 이해 (4) | 2024.10.28 |
[UMC 7th Server] Chapter 4. ES6와 프로젝트 파일 구조의 이해 (1) | 2024.10.28 |