본문으로 바로가기

express로 웹서버를 만드는 것과 그렇게 다르지 않다. res.json()으로 답변을 보내고, 상태코드에 조금 더 신경을 쓰고, TDD를 통해 테스트를 기반으로 API를 테스트해가면서 만들 뿐이다. (TDD는 선택이 아니라 필수라고 생각한다)

 

🚀 req, res 객체

 

express에서 사용하는 요청 객체와 응답 객체는 사실 http 모듈의 request, response 객체를 래핑한 것이다.

주요 사용하는 메소드는 다음과 같다.

 

req.params()

req.query() => offset과 limit를 주로 용함

req.body() => bodyparser가 있어야 사용할 수 있음. express 4.16.0부터 body-paser가 내장되었으므로 설치할 필요는 없음.

// express를 설치했다면 body-parser가 필요 없다
app.use(express.json()); // for parsing application/json
app.use(express.urlencoded({ extended: false })); // for parsing application/x-www-form-urlencoded

// extended가 true면 외부 parser를 사용, false면 사용하지 않음을 의미합니다.
// express에 있는 내장 파서를 사용하므로 false 값을 줍시다

 

res.send() => res.body에 값을 입력할 수 있습니다. res.send({username: "darren"}) 이면 res.body에 username이란 이름으로 값을 넣어줍니다.

res.status() => 상태코드

res.json() => json 형태로 응답함.

res.end() => 끝냄

 

 

🚀 API 구성에 자주 사용하는 상태 코드

 

200 성공적으로 가져옴 GET
201 성공적으로 추가함 POST
204 성공적으로 지워졌음 DELETE
400 요청이 잘못됨. 쿼리에 문자열을 줬다던가, 안 줬던가(undefined)
404 잘못된 페이지. 없는 정보를 찾은 경우
409 중복(Conflict)입니다. POST

 

 

🚀  GET/POST/DELETE/PUT을 가지고 있는 REST API 구현 + TDD

 

REST API를 작성하는 방식은 무조건 테스트 코드를 작성 -> 로직 구현 -> 다른 테스트 코드 작성 -> 로직 구현 ...으로 이루어진다. 빠르게 하겠다고 로직 먼저 구현하면 항상 나중에 유지보수에 문제가 생긴다. 심지어 코딩 도중에도 생긴다. 

 

반드시 TDD를 먼저 하자!

 

또, localhost를 띄워두고 살펴보면서 하자. TDD를 하는 과정에 원본 파일이 변경되기 때문이다.

 

 

app.js

 

보다시피 앱 서버랑 다른게 하나도 없습니다. 그렇다면 무엇이 다른가? router 부분이 다릅니다. 다음에서 살펴봅시다.

const express = require("express");
const logger = require("morgan");
const path = require("path");
const globalRouter = require("./routes/globalRouter");

const app = express();

// midelware
app.use(logger("dev"));
// body-parser 필수다 이거 없으면 res.body를 못쓴다.
app.use(express.json());
app.use(express.urlencoded({ extended: false }));


//view engine
app.set("views", path.join(__dirname, "views")); //views 폴더 생성해야 함
app.set("view engine", "pug");

// route
app.use("/", globalRouter);

const PORT = 3000;

app.listen(PORT, () => console.log(`http://localhost:${PORT}`));

module.exports = app;

 

 

globalRouter.js

 

get, post. delete, put을 전부 사용해서 API를 구성해보았습니다.

req.params, req.query를 사용할 때 parseInt하여 정수형으로 형변환을 해준 것에 대해서 눈여겨봅시다.

const express = require("express");

const globalRouter = express.Router();

// 이후 data fetching으로 대체함
let users = [
  { id: 1, username: "darren" },
  { id: 2, username: "martin" },
  { id: 3, username: "bong" },
];

globalRouter.get("/", (req, res) => res.render("index"));
globalRouter.get("/test", (req, res) => res.render("test"));

// GET users

globalRouter.get("/users", (req, res) => {
  // req.query.limit는 문자열 ex - "2"를 반환하기 때문에 Int로 바꿔줘야 함
  // 만약 limit를 적지 않았을 경우 기본값은 10으로 설정
  // 안하면 undefined 들어가서 오류냄
  req.query.limit = req.query.limit || 10;
  const limit = parseInt(req.query.limit, 10);
  if (Number.isNaN(limit)) {
    return res.status(400).end();
  }
  res.json(users.slice(0, limit));
});

// GET users/:id

globalRouter.get("/users/:id", (req, res) => {
  // 마찬가지로 user.params.id는 문자열이기에 숫자로 변환합니다.
  const id = parseInt(req.params.id, 10);
  // 숫자가 아닌 값이 들어가면 400 에러 처리
  if (Number.isNaN(id)) {
    return res.status(400).end();
  }
  const user = users.filter((user) => {
    return user.id === id;
  })[0];
  // 찾는 유저가 없으면 404 에러 처리
  if (!user) {
    return res.status(404).end();
  }
  res.json(user);
});

// DELETE /users/:id

globalRouter.delete("/users/:id", (req, res) => {
  const id = parseInt(req.params.id, 10);
  if (Number.isNaN(id)) {
    return res.status(400).end();
  }
  // const users로 사용하면 안된다. 그렇게 되면 users를 새로 정의하는 것이 된다.
  // initialize 전에 사용한 셈이므로 오류를 뱉게 된다.
  users = users.filter((user) => user.id !== id);
  return res.status(204).end();
});

// POST /users

globalRouter.post("/users", (req, res) => {
  // form에서 입력받은 username
  const username = req.body.username;
  // 요청한 username이 없을 경우
  if (!username) {
    return res.status(400).end();
  }
  const isConflict =
    users.filter((user) => user.username === username).length > 0;
  if (isConflict) {
    return res.status(409).end();
  }
  const id = Date.now();
  const user = { id, username };
  // 생성한 user를 기존 users에 추가
  users.push(user);
  return res.status(201).json(user);
});

// PUT users/:id
globalRouter.put("/users/:id", (req, res) => {
  const id = parseInt(req.params.id, 10); // 요청한 id
  const username = req.body.username;
  // id가 숫자아 아니면 400 처리
  if (Number.isNaN(id)) {
    return res.status(400).end();
  }
  // 요청한 유저 네임이 없으면 400 처리
  if (!username) {
    return res.status(400).end();
  }
  const user = users.filter((user) => user.id === id)[0];
  // 찾고자 하는 유저가 없으면 404 처리
  if (!user) {
    return res.status(404).end();
  }
  const isConflict =
    users.filter((user) => user.username === username).length > 0;
  // 중복이면 409 Conflict 처리
  if (isConflict) {
    return res.status(409).end();
  }
  user.username = username;

  return res.json(user);
});

module.exports = globalRouter;

 

 

app.spec.js

 

describe => describe(성공/실패) => it(각 처리) 를 통해 TDD를 진행해줍니다. done을 이용하여 비동기 처리하는 것에 주의합시다. res.send({})를 통해 req.body에 값을 넣어줄 수 있음을 눈여겨 봅시다. 

const app = require("./index");
const should = require("should");
const request = require("supertest");

// // GET users
describe("Get users를 불러옵니다.", () => {
  describe("성공시", (done) => {
    it("유저 객체를 담은 배열로 응답한다", (done) => {
      request(app)
        .get("/users")
        .expect(200)
        .end((err, res) => {
          if (err) {
            console.log(err);
          }
          res.body.should.be.instanceof(Array);
          done();
        });
    });
    it("limit 갯수만큼 응답한다", (done) => {
      request(app)
        .get("/users?limit=2")
        .end((err, res) => {
          if (err) {
            console.log(err);
          }
          res.body.should.have.lengthOf(2);
          done();
        });
    });
  });
  describe("실패시", () => {
    it("limit가 숫자형이 아니면 400을 응답한다.", (done) => {
      request(app)
        .get("/users?limit=two")
        .expect(400)
        .end((err, data) => {
          if (err) {
            console.log(err);
          }
          done();
        });
    });
  });
});

// // GET users/:id
describe("GET /users/1은?", () => {
  describe("성공시", () => {
    it("id가 1인 유저 객체를 반환한다.", (done) => {
      request(app)
        .get("/users/1")
        .end((err, res) => {
          if (err) {
            console.log(err);
          }
          res.body.should.have.property("id", 1);
          done();
        });
    });
  });
  describe("실패시", () => {
    it("id가 숫자가 아닐 경우 400으로 응답한다.", (done) => {
      request(app)
        .get("users/two")
        .expect(404)
        .end((err, res) => {
          if (err) {
            console.log(err);
          }
          done();
        });
    });
    it("id로 유저를 찾을 수 없을 경우 404로 응답한다.", (done) => {
      request(app)
        .get("users/999")
        .expect(404)
        .end((err, res) => {
          if (err) {
            console.log(err);
          }
          done();
        });
    });
  });
});

// DELETE users/:id
describe("DELETE /users/1은?", () => {
  describe("성공시", () => {
    it("지우면 204를 응답한다", (done) => {
      request(app).delete("/users/1").expect(204).end(done);
    });
  });
  describe("실패시", () => {
    it("id가 숫자가 아니면 400으로 응답한다", (done) => {
      request(app).delete("/users/one").expect(400).end(done);
    });
  });
});

// // POST users
describe("POST /users/", () => {
  describe("성공시", () => {
    let body;
    it("201 상태 코드를 반환한다", (done) => {
      request(app)
        .post("/users")
        .send({ username: "daniel" })
        .expect(201)
        .end((err, res) => {
          body = res.body;
          done();
        });
    });
    it("생성된 유저 객체를 반환한다.", (done) => {
      body.should.have.property("id");
      done();
    });
    it("입력한 username을 반환한다", (done) => {
      body.should.have.property("username", "daniel");
      done();
    });
  });
  describe("실패시", () => {
    it("username 파라미터 누락시 400을 반환한다.", (done) => {
      request(app).post("/users").send({}).expect(400).end(done);
    });
    it("name이 중복인 경우 409를 반환한다.", (done) => {
      request(app)
        .post("/users")
        .send({ username: "martin" })
        .expect(409)
        .end(done);
    });
  });
});

// PUT users/:id
describe("PUT users/:id", () => {
  describe("성공시", () => {
    it("put을 하면 변경된 유저 객체를 반환합니다.", (done) => {
      request(app)
        .put("/users/2")
        .send({ username: "young" })
        .end((err, res) => {
          console.log(res.body);
          res.body.should.have.property("username", "young");
          done();
        });
    });
  });
  describe("실패시", () => {
    it("정수가 아닌 id일 경우 400을 응답한다.", (done) => {
      request(app).put("/users/what").expect(400).end(done);
    });
    it("찾는 유저 객체가 없는 경우 400 응답", (done) => {
      request(app).put("/users/1").send({}).expect(400).end(done);
    });
    it("없는 유저일 경우 404 응답", (done) => {
      request(app)
        .put("/users/")
        .send({ username: 10000000 })
        .expect(404)
        .end(done);
    });
    it("이름이 중복일 경우 409를 응답한다", (done) => {
      request(app)
        .put("/users/3")
        .send({ username: "bong" })
        .expect(409)
        .end(done);
    });
  });
});

 

 

🚀 라우팅 처리

 

yts에서도 보면, 일반적인 웹 사이트였다가 api로 접속하면 링크가 바뀝니다.

https://yts.mx/

https://yts.mx/api/v2/list_movies.json?page=1

 

현재 localhost:3000이라면

api로 접속했을 때는 localhost:3000/api/users와 같은 경로로 접속하는 게 좋습니다.

 

이와 같은 라우팅을 위해 다음과 같이 경로를 수정해주었습니다. 여기서 user.ctrl.js은 컨트롤러단을 분리한 것인데, 제 취향에는 분리하지 않는 게 더 편한 것 같습니다.

 

 

 

 

 

🚀 DB와 연동

 

 

sequelize ORM를 이용한 MySQL 활용

ORM는 우리가 사용하는 프로그래밍 언어를 SQL문으로 변환해준다. python의 경우 장고 ORM, node.js 에는 type ORM, 시퀄라이즈(sequelize) 등이 존재한다. SQL문을 작성하지 않고 평소 하던 JS나 python으로 DB를..

darrengwon.tistory.com

 

이번에는 RDS Mysql에 GUI는 Heidi SQL을 사용해보았다.

특이사항은 없으며 sequelize에 사용되는 config.json을 다음과 같이 세팅하면 된다.

  "development": {
    "username": "RDS 아이디",
    "password": "RDS 비밀번호",
    "database": "생성, 사용하고자 하는 DB 이름",
    "host": "RDS 엔드 포인트",
    "dialect": "mysql",
    "operatorsAliases": false
  },

 

싱크를 한 다음에는 각 로직에서 활용하면 된다. 간단하게 일부분만 살펴보자.

 

모델링한 테이블에서 정보를 가져오면 됩니다. 비동기적으로 처리한 것을 주의합시다.

const db = require("../../models");
const User = db.User;

user.get("/users", (req, res) => {
  req.query.limit = req.query.limit || 10;
  const limit = parseInt(req.query.limit, 10);
  if (Number.isNaN(limit)) {
    return res.status(400).end();
  }

  User.findAll({ limit: limit }).then((users) => {
    res.json(users);
  });
});

 

테스트 부분입니다. 

테스트하기 전에 sync를 해줘야 합니다. 또, 테스트 할 때는 force:true값을 주는 것이 좋습니다. 

또, 매번 테스트할 때마다 동일한 환경을 조성해야하기 때문에 testdata, before과 같은 처리를 통해 더미 데이터를 넣어주는 것을 매 describe때마다 하는 것이 좋습니다.

const db = require("../../models");
const User = db.User;

// // GET users
describe("Get users를 불러옵니다.", () => {
  const testdata = [
    { username: "darren" },
    { username: "martin" },
    { username: "bong" },
  ];
  before(() => {
    return db.sequelize.sync({ force: true });
  });
  before(() => {
    return User.bulkCreate(testdata);
  });
  describe.only("성공시", () => {
    it("유저 객체를 담은 배열로 응답한다", (done) => {
      request(app)
        .get("/api/users")
        .expect(200)
        .end((err, res) => {
          if (err) {
            console.log(err);
          }
          console.log(res.body);
          res.body.should.be.instanceof(Array);
          done();
        });
    });
    it("limit 갯수만큼 응답한다", (done) => {
      request(app)
        .get("/api/users?limit=2")
        .end((err, res) => {
          if (err) {
            console.log(err);
          }
          res.body.should.have.lengthOf(2);
          done();
        });
    });
  });

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체