Super Kawaii Cute Cat Kaoani
본문 바로가기
🏃‍♀️ 대외활동/UMC 7기 - Node.js

[UMC 7th Server] Chapter 7. Express 미들웨어 & API 응답 통일 & 에러 핸들링

by wonee1 2024. 11. 12.
728x90


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

☑️ 실행결과 (오류 핸들러 정상 작동 )

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90