HJW's IT Blog

[CodeIt] 07월 2주차 본문

카테고리 없음

[CodeIt] 07월 2주차

kiki1875 2024. 7. 11. 11:37

필요한 데이터 관리 + 프론트엔드에 전송 → 이를 백엔드 서버라 부른다

HTTP 메소드 + URL = 엔드포인트

엔드포인트의 집함 = API

REST API

NodeJS → 웹 브라우저 바깥에서 JS 를 사용할 수 있게 해주는 프레임워크

EXPRESS → JS 백엔드 개발에서 가장 유명한 라이브러리

  • Req 과 res 를 쉽게 다룰 수 있음
  • 특정 구조를 고집하지 않고 최소한의 기능만 제공

MongoDB → 데이터를 테이블이 아닌 문서의 형태로 저장

  • 문서 하나 = document
  • 문서의 모음 = collection

 

예제 (ToDo) API 문서

엔드포인트

   GET /tasks
   GET /tasks/:id
  POST /tasks
DELETE /tasks/:id
 PATCH /tasks/:id

TASK 객체

  • 속성
    • _id: string (Task 객체의 고유 식별자)
    • title: string (Task 제목, 최대 30자)
    • description : string (테스크 상세설명 , optional)
    • isComplete : boolean (테스크 완료 여부, 자동생성 필드)
    • createdAt : string (테스크 생성 시점 타임스템프)
    • updatedAt : string (테스크 수정 시점 타임스템프)
  • 객체 예시
{
  "_id": "641bf3c9c0a964b65289902a",
  "title": "파이썬 공부",
  "description": "프로그래밍 시작하기 in Python 토픽 끝내기",
  "isComplete": false,
  "createdAt": "2023-03-23T06:34:11.617Z",
  "updatedAt": "2023-03-23T06:34:11.617Z"
}

TASK 목록 조회

요청

GET /tasks

반환

200 OK
Content-Type: application/json

[
  {
    "_id": "641bf3c9c0a964b65289902a",
    "title": "파이썬 공부",
    "description": "프로그래밍 시작하기 in Python 토픽 끝내기",
    "isComplete": false,
    "createdAt": "2023-03-23T06:34:11.617Z",
    "updatedAt": "2023-03-23T06:34:11.617Z"
  },
  {
    "_id": "641bf3c9c0a964b65289903c",
    "title": "독서",
    "description": "30 페이지",
    "isComplete": false,
    "createdAt": "2023-03-23T06:34:10.617Z",
    "updatedAt": "2023-03-23T06:34:10.617Z"
  },
  ...
]

TASK 객체 전체 목록을 최신순으로 반환

TASK 조회

요청

GET /tasks/:id

GET <http://localhost:3000/tasks/641bf3c9c0a964b65289902a>

반환

200 OK
Content-Type: application/json

{
  "_id": "641bf3c9c0a964b65289902a",
  "title": "파이썬 공부",
  "description": "프로그래밍 시작하기 in Python 토픽 끝내기",
  "isComplete": false,
  "createdAt": "2023-03-23T06:34:11.617Z",
  "updatedAt": "2023-03-23T06:34:11.617Z"
}

TASK 생성

// 요청
POST /tasks

POST <http://localhost:3000/tasks>
Content-Type: application/json

{
  "title": "강아지 산책",
  "description": "강아지랑 30분 산책하기"
}

// 반환
201 Created
Content-Type: application/json

{
  "_id": "641bf3c9c0a964b65289902d",
  "title": "강아지 산책",
  "description": "강아지랑 30분 산책하기",
  "isComplete": false,
  "createdAt": "2023-04-17T05:41:35.319Z",
  "updatedAt": "2023-04-17T05:41:35.319Z"
}

TASK 수정

// 요청
PATCH /tasks/:id

PATCH <http://localhost:3000/tasks/641bf3c9c0a964b65289902a>
Content-Type: application/json

{
  "title": "자바스크립트 공부",
  "description": "프로그래밍 시작하기 in JavaScript 토픽 끝내기",
}

// 반환
200 OK
Content-Type: application/json

{
  "_id": "641bf3c9c0a964b65289902a",
  "title": "자바스크립트 공부",
  "description": "프로그래밍 시작하기 in JavaScript 토픽 끝내기",
  "isComplete": false,
  "createdAt": "2023-03-23T06:34:11.617Z",
  "updatedAt": "2023-03-24T06:34:11.617Z"
}

TASK 삭제

// 요청
DELETE /tasks/:id

// 예시
DELETE <http://localhost:3000/tasks/641bf3c9c0a964b65289902a>

// 반환
204 No Content
 

EXPRESS 로 API 서버 (환경설정)

https://nodejs.org/en

  • nodejs 설치
npm install express

npm install --save-dev nodemon

  • package.json 에 다음 추가
    • "type": "module”

시작하기

import express from 'express';

const app = express();

app 을 통해 이제 라우트를 생성할 수 있다

app.get('/hello', (req,res) => {});

url 경로 + 콜백함수 의 구조이다

  • request handler 라고도 불린다
  • req handler 는 두개의 파라미터를 가지는데, request 와 response 이다
app.listen(3000, () => console.log('Server Started'));
  • 3000 은 port number 이며, 에플리케이션이 실행되면, 콜백함수가 실행된다.

쿼리스트링

URL 에서 ? 뒤에 오는 부분

예시

<http://localhost:3000/tasks?sort=oldest&count=3>
app.get('/tasks', (req,res) => {

  const sort = req.query.sort;
  const count = Number(req.query.count);

  const compareFn = 
    sort === 'oldest'
      ? (a, b) => a.createdAt - b.createdAt
      : (a, b) => b.createdAt - a.createdAt;
  
  let newTasks = tasks.sort(compareFn);

  if(count){
    newTasks = newTasks.slice(0, count);
  }

  res.send(newTasks);
});
  • 쿼리파라미터를 받아온 뒤 compareFn 을 통해 정렬
  • count를 받아와 slicing 후 해당 객체 반환

Dynamic URL

  • 일정하지 않고 일부가 변화하는 url
GET <http://localhost:3000/tasks/2>
app.get('/tasks/:id', (req, res)=> {
  const id = Number(req.params.id);
  const task = tasks.find((task) => task.id === id);
  if(task){
    res.send(task);
  }else{
    res.status(404).send({message: 'cannot find given id.'});
  }

});

바뀌는 부분을 : 을 사용하여 나타낸다

req 의 params 에 저장

POST REQUEST

app.use(express.json());
  • 이를 통해 파싱을 해주어야 한다
  • req 의 type 이 application/json이라면 body 를 파싱한다는 뜻

PATCH REQUEST

app.patch('/tasks/:id', (req, res)=> {
  const id = Number(req.params.id);
  const task = tasks.find((task) => task.id === id);
  if(task){
    Object.keys(req.body).forEach((key)=>{
      task[key] = req.body[key];
    });
    task.updatedAt = new Date();
    res.send(task);
  }else{
    res.status(404).send({message: 'cannot find given id.'});
  }
});

DELETE REQUEST

app.delete('/tasks/:id', (req, res)=> {
  const id = Number(req.params.id);
  const idx = tasks.findIndex((task) => task.id === id);
  if(idx >= 0){
    tasks.splice(idx, 1);
    res.sendStatus(204);
  }else{
    res.status(404).send({message: 'cannot find given id.'});
  }
});

Mongoose

  • schema 를 생성해야 한다
//schema 생성 코드
import mongoose from "mongoose";

const TaskSchema = new mongoose.Schema(
  {
    title: {
      type: String,
    },
    description: {
      type: String,
    },
    isComplete: {
      type: Boolean,
      default: false,
    },
  },
  {
    timestamps: true, 
  }
)

const Task = mongoose.model('Task', TaskSchema);

export default Task;
  • timestamps 사용시 mongoose 가 자동으로 createdAt, updated At 생성
  • model 은 schema를 기반으로, 데이터를 수정, 삽입, 삭제, 조회할수 있는 interface
  • 'Task' 는 tasks 라는 collection 의 데이터를 다룬다
  • seed data → 초기 데이터
  • seeding → 초기데이터 삽입 과정
// package.json
"seed": "node data/seed.js" 추가
// seed.js
import mongoose from "mongoose";
import data from './mock.js'
import Task from '../models/Task.js'
import { DATABASE_URL } from "../env.js";

mongoose.connect(DATABASE_URL);

await Task.deleteMany({});
await Task.insertMany(data);

mongoose.connection.close();

이후, 터미널에서 npm run seed

// mongoose 를 이용한 get task

app.get('/tasks', async (req,res) => {

  const sort = req.query.sort;
  const count = Number(req.query.count) || 0;

  const sortOption = {
    createdAt: sort === 'oldest' ? 'asc' : 'desc'
  };
  const tasks = await Task.find().sort(sortOption).limit(count); // 모든 task 객체를 가져온다, sortOption으로 정렬, count 개 만큼
  res.send(tasks);
});

Query Filter

// 필드의 값이 특정값과 일치하는지 확인
Person.find({ name: 'James' });

//필드가 특정값 초과 : $gt, 특정 값 미만 : $lt
Person.find({ age: { $gt: 35 } });

// 문자열 필드가 특정 패턴을 가지는지
Person.find({ email: { $regex: 'gmail\\.com$' } });

데이터 생성 및 유효성 검사

// 데이터 생성

app.post('/tasks', async (req, res) =>{
  const newTask = await Task.create(req.body);
  res.status(201).send(newTask);
});
// 만약 필수로 필요로 하는 필드가 있다면
// 스키마에 다음과 같이 옵션을 주면 된다
// validate 는 제목이 최소 두 단어인지 검증하는 함수이다
title: {
  type: String,
  required: true,
  maxLength: 30,
  validate: {
    validator: function (title){
      return title.split(' '.length) > 1;
    },
    message: 'Must Contain At Least 2 Words.',
  }
},

<aside> 💡 mongoose 는 스키마에 정의되지 않은 필드는 무시한다

</aside>

  • 잘못된 데이터로 validation 오류가 생기면 서버가 죽는다
  • 다음과 같이 비동기 핸들러를 구현하여 해결할 수 있다
function asyncHandler(handler){
  return async function (req, res){
    try{
      await handler(req, res);
    } catch(e){
      if(e.name === 'ValidationError'){
        res.status(400).send({message: e.message});
      }else if(e.name === 'CastError'){
        res.status(404).send({message: 'Cannot find given id.'});
      }else{
        res.status(500).send({message: e.message});
      }
    }
  }
}

app.post('/tasks', asyncHandler(async (req, res) =>{
  const newTask = await Task.create(req.body);
  res.status(201).send(newTask);
}));
  • 데이터 수정 및 삭제
app.patch('/tasks/:id', async (req, res)=> {
  const id = req.params.id;
  const task = await Task.findById(id);
  if(task){
    Object.keys(req.body).forEach((key)=>{
      task[key] = req.body[key];
    });
    res.send(task);
  }else{
    res.status(404).send({message: 'cannot find given id.'});
  }

});

app.delete('/tasks/:id', async (req, res)=> {
  const id = req.params.id;
  const task = await Task.findByIdAndDelete(id);
  if(task){
    res.sendStatus(204);
  }else{
    res.status(404).send({message: 'cannot find given id.'});
  }
});

\

환경 변수

  • 프로그램이 실행되는 환경과 관련이 있는 변수들
  • ex) DATABASE_URL

dotenv 로 환경변수 다루기

npm install dotenv

.env 파일 생성

DATABASE_URL="mongodb+srv://<username>:<password>@todo-api.l0aepsl.mongodb.net/todo-api?retryWrites=true&w=majority"
PORT=3000

// app.js

import * as dotenv from 'dotenv';
dotenv.config();

// ...

mongoose.connect(process.env.DATABASE_URL).then(() => console.log('Connected to DB'));

// ...

app.listen(process.env.PORT || 3000, () => console.log('Server Started'));
// seed.js

// ...

import * as dotenv from 'dotenv';
dotenv.config();

// ...

mongoose.connect(process.env.DATABASE_URL);

  • CORS 오류
    • 현 상태로 배포하게 되면 프론트엔드에서 api 사용 불가, cors error 가 뜬다
npm install cors
// ...

import cors from 'cors';

// ...

const app = express();

app.use(cors());
app.use(express.json());

  • 특정 주소만 cors 배포를 허용할 수 도 있다
const app = express();

const corsOptions = {
  origin: ['', '<https://my-todo.com>'],
};

app.use(cors(corsOptions));
app.use(express.json());