Super Kawaii Cute Cat Kaoani
본문 바로가기
{Extracurricular Activities}/UMC 7기 - Node.js

[UMC 7th Server] Chapter 5. ES6와 프로젝트 파일 구조의 이해

by wonee1 2024. 10. 28.
728x90

 

Chapter 5. ES6와 프로젝트 파일 구조의 이해 워크북 

 

 

 

☑️ 실습 인증


 

API 개발하기 실습 및 DTO 추가구현

 

 

 

👉 라이브러리 설치하기

 

 

이번 주차의 실습에 필요한 라이브러리들 설치해보았습니다.

npm install \
cors \
dotenv \
http-status-codes \
mysql2

 

 

다음 명령어 실행시 다음과 같은 오류 발생⚠️

 

 

 

 

 

다음과 같이 설치 명렁어 수정하여 설치 진행했습니다 🔽

npm install cors dotenv http-status-codes mysql2

 

 

 

 

 

 

 

👉 환경 변수 넣기 (.env 파일)

 

 

1. .env 파일에 mysql 비밀번호, 프로젝트 이름을 입력해주었고  gititgnore로 이 파일들을 ignore해주었습니다. 

 

 

2.  또한 index.js 파일에 3000 포트로 서버 포트가 구성되지만, 앞으로는 환경 변수를 통해 변경할 수 있도록 하기 위해서, index.js 파일을 다음과 같이 수정해주었습니다.

 

 

 

 

 

👉 DB 연결하기

 

src 폴더에 db.config.js 파일을 생성한 뒤에 여러가지 설정을 해주었습니다.

 

+ 특히 connection pool을 사용하기 위한 설정도 같이 입력해주었습니다.

 

 

👉 DTO 구현 (추가)

 

export const responseFromUser = (user) => {
  return {
    email: user.email,
    name: user.name,
    gender: user.gender,
    birth: user.birth ? user.birth.toISOString().split("T")[0] : "", // 'YYYY-MM-DD' 형식으로 변환
    address: user.address || "",
    detailAddress: user.detailAddress || "",
    phoneNumber: user.phoneNumber,
    preferences: user.preferences,
  };
};

 

 

 

 


 

 

 

🎯핵심 키워드 정리


 

 

🔹 환경 변수

  • . DB에 연결하기 위한 DB 호스트 및 계정 정보, 다른 서버와 연동하는 경우에는 해당 서버의 주소 정보, API Key 정보 등
  • .env 파일에 저장한다

 

 

🔹CORS (교차 출처 리소스 공유)

  • CORS를 설정한다는 건 ‘출처가 다른 서버 간의 리소스 공유’를 허용한다는 뜻 
  • 출처가 다르더라도 요청과 응답을 주고받을 수 있도록 서버에 리소스 호출이 허용된 출처(Origin)를 명시해 주는 방식

 

🔹 CORS 에러 대응

  • 서버에서 Access-Control-Allow-Origin 헤더를 설정
  • 프록시 서버 사용하여 웹 애플리케이션이 리소스와 동일한 출처에서 요청을 보내는 것처럼 보이므로 CORS 에러를 방지

 

🔹 DB Connection

  • DB를 사용하기 위해 DB와 애플리케이션 간 통신을 할 수 있는 수단
  • DB Connection은 Database Driver와 Database 연결 정보를 담은 URL이 필요함
  • Java의 DB Connection은 JDBC를 주로 이용하는데, URL 타입을 사용함

🔹 DB Connection Pool

  • JDBC API를 사용하여 데이터베이스와 연결하기 위해 Connection 객체를 생성하는 작업은 비용이 굉장히 많이 드는 작업 중 하나이다.
  • 이런 문제를 해결하기 위해서 커넥션풀(DBCP)을 통해 이미 연결하는 작업을 pool에 있기 때문에 그것을 재사용하는 것이다.

 

 

🔹비동기란?

어떤 작업을 실행할 때 그 작업이 완료되지 않더라도 다음 코드를 실행하는 방식을 의미한다. 즉, 작업이 완료되지 않았더라도 결과를 기다리지 않고 다음 코드를 실행하는 것이다. 이러한 방식은 작업이 오래 걸리는 경우 시간을 절약하고, 병렬적인 작업 처리가 가능하다.

 

🔹async

  • async는 함수의 앞에 붙여서 해당 함수가 비동기 함수임을 나타내며, await는 비동기 함수의 실행 결과를 기다리는 키워드

🔹await

  • await 키워드는 Promise 객체가 완료될 때까지 코드 실행을 일시 중지하므로, try-catch 블록 안에서 사용하여 에러 처리를 할 수 있다

 

+동기란?

어떤 작업을 실행할 때 그 작업이 끝나기를 기다리는 방식을 의미한다. 즉, 작업이 완료될 때까지 다음 코드의 실행을 멈추고 기다리는 것이다. 이러한 방식은 작업의 순서를 보장하고, 작업이 끝날 때까지 결과를 기다리는 것이 가능하다

 

 

 

 

 

 

 

 

 

 


 

 

 

🔥 미션 인증


 

0. 파일 구조 셋팅 

 

 

파일구조

 

 

  • src 안에 controller, dto, repository, sevice 폴더를 넣어줬습니다.
  • index.js, db.config.js도 src 폴더 안에 넣어줬습니다. 
  • test.http는 리퀘스트를 보내고 확인할 용도의 파일입니다. 

 

 

 

 

1. 특정 지역에 가게 추가하기 API

 

 

 

파일 구조 설정

  • src/controller/store.controller.js
  • src/dtos/store.dto.js
  • src/repositories/store.repository.js
  • src/services/store.service.js

 

store.controller.js

// src/controller/store.controller.js
import { StatusCodes } from "http-status-codes";
import { bodyToStore } from "../dtos/store.dto.js";
import { storeSignUp } from "../services/store.service.js";

export const handleStoreSignUp = async (req, res, next) => {
    try {
        const store = await storeSignUp(bodyToStore(req.body));
        res.status(StatusCodes.CREATED).json({ result: store });
    } catch (error) {
        res.status(StatusCodes.BAD_REQUEST).json({ error: error.message });
    }
};

 

store.dto.js

// src/dtos/store.dto.js
export const bodyToStore = (body) => {
    return {
        storeName: body.storeName,
        address: body.address,
        regionId: body.regionId,
    };
};

export const responseFromStore = (store) => {
    return {
        storeId: store.storeId,
        storeName: store.storeName,
        address: store.address,
        regionId: store.regionId,
    };
};

 

store.service.js

// src/services/store.service.js
import { addStore } from "../repositories/store.repository.js";
import { responseFromStore } from "../dtos/store.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 });
};

 

 

store.repository.js

// src/repositories/store.repository.js
import { pool } from "../db.config.js";

export const addStore = async (data) => {
    const conn = await pool.getConnection();

    try {
        const [result] = await pool.query(
            `INSERT INTO stores (store_name, store_address, region_id) VALUES (?, ?, ?);`,
            [data.storeName, data.address, data.regionId]
        );
        return result.insertId;
    } catch (err) {
        throw new Error(`오류가 발생했어요. 요청 파라미터를 확인해주세요. (${err})`);
    } finally {
        conn.release();
    }
};

 

 

 

 

 

2. 가게에 리뷰 추가하기 API

 

 

파일 구조 설정

  • src/controller/review.controller.js
  • src/dtos/review.dto.js
  • src/repositories/review.repository.js
  • src/services/review.service.js

 

review.dto.js

// src/dtos/review.dto.js
export const bodyToReview = (body) => {
    return {
        userId: body.userId,
        storeId: body.storeId,
        rating: body.rating,
        reviewText: body.reviewText,
    };
};

export const responseFromReview = (review) => {
    return {
        reviewId: review.reviewId,
        userId: review.userId,
        storeId: review.storeId,
        rating: review.rating,
        reviewText: review.reviewText,
    };
};

 

 

review.repository.js

// src/repositories/review.repository.js
import { pool } from "../db.config.js";

export const addReview = async (data) => {
    const conn = await pool.getConnection();

    try {
        const [result] = await pool.query(
            `INSERT INTO reviews (user_id, store_id, rating, review_text) VALUES (?, ?, ?, ?);`,
            [data.userId, data.storeId, data.rating, data.reviewText]
        );
        return result.insertId;
    } catch (err) {
        throw new Error(`오류가 발생했어요. 요청 파라미터를 확인해주세요. (${err})`);
    } finally {
        conn.release();
    }
};

 

 

review.service.js

// src/services/review.service.js
import { addReview } from "../repositories/review.repository.js";
import { responseFromReview } from "../dtos/review.dto.js";

export const reviewSignUp = async (data) => {
    const reviewId = await addReview({
        userId: data.userId,
        storeId: data.storeId,
        rating: data.rating,
        reviewText: data.reviewText,
    });

    if (!reviewId) {
        throw new Error("리뷰를 추가할 수 없습니다.");
    }

    return responseFromReview({ reviewId, ...data });
};

 

 

review.controller.js

// src/controller/review.controller.js
import { StatusCodes } from "http-status-codes";
import { bodyToReview } from "../dtos/review.dto.js";
import { reviewSignUp } from "../services/review.service.js";

export const handleReviewSignUp = async (req, res, next) => {
    try {
        const review = await reviewSignUp(bodyToReview(req.body));
        res.status(StatusCodes.CREATED).json({ result: review });
    } catch (error) {
        res.status(StatusCodes.BAD_REQUEST).json({ error: error.message });
    }
};

 

 

 

 

 

 

3. 가게에 리뷰 추가하기 API

 

 

파일 구조 설정

  • src/controller/review.controller.js
  • src/dtos/review.dto.js
  • src/repositories/review.repository.js
  • src/services/review.service.js

 

review.dto.js

// src/dtos/review.dto.js
export const bodyToReview = (body) => {
    return {
        userId: body.userId,
        storeId: body.storeId,
        rating: body.rating,
        reviewText: body.reviewText,
    };
};

export const responseFromReview = (review) => {
    return {
        reviewId: review.reviewId,
        userId: review.userId,
        storeId: review.storeId,
        rating: review.rating,
        reviewText: review.reviewText,
    };
};

 

 

review.repository.js

// src/repositories/review.repository.js
import { pool } from "../db.config.js";

export const addReview = async (data) => {
    const conn = await pool.getConnection();

    try {
        const [result] = await pool.query(
            `INSERT INTO reviews (user_id, store_id, rating, review_text) VALUES (?, ?, ?, ?);`,
            [data.userId, data.storeId, data.rating, data.reviewText]
        );
        return result.insertId;
    } catch (err) {
        throw new Error(`오류가 발생했어요. 요청 파라미터를 확인해주세요. (${err})`);
    } finally {
        conn.release();
    }
};

 

 

review.service.js

// src/services/review.service.js
import { addReview } from "../repositories/review.repository.js";
import { responseFromReview } from "../dtos/review.dto.js";

export const reviewSignUp = async (data) => {
    const reviewId = await addReview({
        userId: data.userId,
        storeId: data.storeId,
        rating: data.rating,
        reviewText: data.reviewText,
    });

    if (!reviewId) {
        throw new Error("리뷰를 추가할 수 없습니다.");
    }

    return responseFromReview({ reviewId, ...data });
};

 

 

review.controller.js

// src/controller/review.controller.js
import { StatusCodes } from "http-status-codes";
import { bodyToReview } from "../dtos/review.dto.js";
import { reviewSignUp } from "../services/review.service.js";

export const handleReviewSignUp = async (req, res, next) => {
    try {
        const review = await reviewSignUp(bodyToReview(req.body));
        res.status(StatusCodes.CREATED).json({ result: review });
    } catch (error) {
        res.status(StatusCodes.BAD_REQUEST).json({ error: error.message });
    }
};

 

 

 

 

 

4. 가게에 미션 추가하기 API

 

 

파일 구조 설정

  • src/controller/mission.controller.js
  • src/dtos/mission.dto.js
  • src/repositories/mission.repository.js
  • src/services/mission.service.js

mission.dto.js

// src/dtos/mission.dto.js
export const bodyToMission = (body) => {
    return {
        regionId: body.regionId,
        description: body.description,
        missionStatus: body.missionStatus
    };
};

export const responseFromMission = (mission) => {
    return {
        missionId: mission.missionId,
        regionId: mission.regionId,
        description: mission.description,
        missionStatus: mission.missionStatus
    };
};

 

mission.repository.js

// src/repositories/mission.repository.js
import { pool } from "../db.config.js";

export const addMission = async (data) => {
    const conn = await pool.getConnection();

    try {
        const [result] = await pool.query(
            `INSERT INTO missions (region_id, description, mission_status) VALUES (?, ?, ?);`,
            [data.regionId, data.description, data.missionStatus]
        );
        return result.insertId;
    } catch (err) {
        throw new Error(`오류가 발생했어요. 요청 파라미터를 확인해주세요. (${err})`);
    } finally {
        conn.release();
    }
};

 

mission.service.js

// src/services/mission.service.js
import { addMission } from "../repositories/mission.repository.js";
import { responseFromMission } from "../dtos/mission.dto.js";

export const missionSignUp = async (data) => {
    const missionId = await addMission(data);
    if (!missionId) {
        throw new Error("미션을 추가할 수 없습니다.");
    }
    return responseFromMission({ missionId, ...data });
};

 

mission.controller.js

// src/controller/mission.controller.js
import { StatusCodes } from "http-status-codes";
import { bodyToMission } from "../dtos/mission.dto.js";
import { missionSignUp } from "../services/mission.service.js";

export const handleMissionSignUp = async (req, res, next) => {
    try {
        const mission = await missionSignUp(bodyToMission(req.body));
        res.status(StatusCodes.CREATED).json({ result: mission });
    } catch (error) {
        res.status(StatusCodes.BAD_REQUEST).json({ error: error.message });
    }
};

 

 

 

 

4. 가게의 미션을 도전 중인 미션에 추가 (미션 도전하기) API

 

파일 구조 설정

  • src/controller/challenge.controller.js
  • src/dtos/challenge.dto.js
  • src/repositories/challenge.repository.js
  • src/services/challenge.service.js

challenge.dto.js

// src/dtos/challenge.dto.js
export const bodyToChallenge = (body) => {
    return {
        userId: body.userId,
        missionId: body.missionId,
        storeId: body.storeId,
        status: "진행 중"
    };
};

export const responseFromChallenge = (challenge) => {
    return {
        userMissionId: challenge.userMissionId,
        userId: challenge.userId,
        missionId: challenge.missionId,
        storeId: challenge.storeId,
        status: challenge.status,
        completedAt: challenge.completedAt,
    };
};

 

 

challenge.repository.js

// src/repositories/challenge.repository.js
import { pool } from "../db.config.js";

export const addChallenge = async (data) => {
    const conn = await pool.getConnection();

    try {
        // 중복 도전 확인
        const [existing] = await pool.query(
            `SELECT * FROM user_missions WHERE user_id = ? AND mission_id = ? AND status = '진행 중';`,
            [data.userId, data.missionId]
        );

        if (existing.length > 0) {
            throw new Error("이미 도전 중인 미션입니다.");
        }

        // 도전 추가
        const [result] = await pool.query(
            `INSERT INTO user_missions (user_id, mission_id, store_id, status) VALUES (?, ?, ?, ?);`,
            [data.userId, data.missionId, data.storeId, data.status]
        );

        return result.insertId;
    } catch (err) {
        throw new Error(`오류가 발생했습니다. 요청 파라미터를 확인해주세요. (${err})`);
    } finally {
        conn.release();
    }
};

 

challenge.service.js

// src/services/challenge.service.js
import { addChallenge } from "../repositories/challenge.repository.js";
import { responseFromChallenge } from "../dtos/challenge.dto.js";

export const challengeSignUp = async (data) => {
    const userMissionId = await addChallenge(data);

    if (!userMissionId) {
        throw new Error("미션 도전에 실패했습니다.");
    }

    return responseFromChallenge({ userMissionId, ...data });
};

 

challenge.controller.js

// src/controller/challenge.controller.js
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).json({ result: challenge });
    } catch (error) {
        res.status(StatusCodes.BAD_REQUEST).json({ error: error.message });
    }
};

 

 

 

5. test.http 파일에서 각 api 호출 확인

 

 

  1. 특정 지역에 가게 추가하기 api 

☑️리퀘스트

### 가게 추가 요청
POST <http://localhost:3000/api/stores>
Content-Type: application/json

{
  "storeName": "Awesome Cafe",
  "address": "123 Coffee Street",
  "regionId": 1
}

☑️실행 결과

HTTP/1.1 201 Created
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 94
ETag: W/"5e-ZTlgx3YwQloWF9jnryv4eWH/JuM"
Date: Mon, 21 Oct 2024 09:53:56 GMT
Connection: close

{
  "result": {
    "storeId": 2,
    "storeName": "Awesome Cafe",
    "address": "123 Coffee Street",
    "regionId": 1
  }
}

 

 

 

리퀘스트 확인

 

 

     2.  가게 리뷰 추가 요청 api

 

 

☑️리퀘스트

### 가게 리뷰 추가 요청
POST <http://localhost:3000/api/reviews>
Content-Type: application/json

{
  "userId": 1,
  "storeId": 1,
  "rating": 5,
  "reviewText": "Great coffee and ambiance!"
}

 

☑️실행 결과

HTTP/1.1 201 Created
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 101
ETag: W/"65-DuLjQ2qzFskb00ZbxsoIhTAQNmo"
Date: Mon, 21 Oct 2024 09:54:57 GMT
Connection: close

{
  "result": {
    "reviewId": 4,
    "userId": 1,
    "storeId": 1,
    "rating": 5,
    "reviewText": "Great coffee and ambiance!"
  }
}

 

 

 

 

 

    3. 가게에 미션 추가하기 요청 api

 

 

☑️리퀘스트

### 가게 미션 추가 요청
POST <http://localhost:3000/api/missions>
Content-Type: application/json

{
  "regionId": 1,
  "description": "Visit all cafes in the area",
  "missionStatus": "수행 중"
}

☑️실행 결과

HTTP/1.1 201 Created
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 112
ETag: W/"70-AH97W/U0QAP0sYsiaxn+F831t9g"
Date: Mon, 21 Oct 2024 09:55:17 GMT
Connection: close

{
  "result": {
    "missionId": 2,
    "regionId": 1,
    "description": "Visit all cafes in the area",
    "missionStatus": "수행 중"
  }
}

 

 

 

 

 

 

   4. 가게의 미션을 도전 중인 미션에 추가 (미션 도전하기) API

 

 

 

☑️리퀘스트

### 미션 도전 요청
POST <http://localhost:3000/api/challenges>
Content-Type: application/json

{
  "userId": 1,
  "missionId": 1,
  "storeId": 1
}

☑️실행결과

  1. 처음 도전 요청을 실행했을 때
HTTP/1.1 201 Created
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 89
ETag: W/"59-mY5lr7TUJsr5RLYcKwhCI1tUJ4"
Date: Mon, 21 Oct 2024 09:53:35 GMT
Connection: close

{
  "result": {
    "userMissionId": 5,
    "userId": 1,
    "missionId": 1,
    "storeId": 1,
    "status": "진행 중"
  }
}

 

 

 

 

 

    2. 중복 요청할 시 이미 도전 중인 미션이라고 알림

HTTP/1.1 400 Bad Request
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 131
ETag: W/"83-Evid6YRHEYibV76wzM3M6kBsLoc"
Date: Mon, 21 Oct 2024 09:56:09 GMT
Connection: close

{
  "error": "오류가 발생했습니다. 요청 파라미터를 확인해주세요. (Error: 이미 도전 중인 미션입니다.)"
}

 

 

 

 

 

 

 

 

 

📢 5주차 학습 후기


 

🐱: 이번 주 미션에서 저번 시간에 설계했던 API URL 을 기반으로 API를 추가적으로 설계하는 시간을 가졌습니다. 전 동아리에서는 DTO에 대해서는 배우지 않았는데 ,이번 시건에 DTO에 대해서 배우고 직접 그에 맞는 코드도 짜보면서 백엔드에 필요한 코드 구성을 익힐 수 있었습니다.

또한 API 설계 후 직접 리퀘스트를 보내 API 리스폰스가 잘 작동하는 지 확인하면서 점검까지 해보는 경험을 해보면서 api 설계에 대한 지식을 쌓을 수 있어서 좋았습니다.

 

728x90