Node.js/Express.js

[Toy Project - Omuk] 8. 데이터 베이스 쿼리 작성

턴태 2022. 9. 5. 17:07

연결 수정

 

각 path마다 다른 모델의 쿼리를 실행하고자 모델을 새롭게 정의하겠습니다. 그렇기 때문에 기존 pool 말고 모델 생성하는 것을 목표로 코드를 수정했습니다.

 

Sequelize

config.json 파일을 작성하여 설정할 것을 저장해주었습니다. 위치는 루트 디렉터리 하위에 config 폴더를 만들어 저장했습니다.

 

.
│	📄 기타 환경 파일들
├── .env
├── package.json
├── package-lock.json
├── node_modules
│
│	💻메인 파일들
├── app.js
├── config
│   └── config.json
├── lib
│   └── handler.js
├── models
│   └── index.js
├── public
│   ├── images
│   │	└── logo.png
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── auth.js
└── views
    ├── home.handlebars
    └── layouts
    	├── main.handlebars
        └── test.handlebars

 

{
  "development": {
    "username": "postgres",
    "password": "password",
    "database": "omuk",
    "host": "localhost",
    "dialect": "postgres",
    "port": 5432
  },
  "test": {
    "username": "postgres",
    "password": "password",
    "database": "omuk_test",
    "host": "localhost",
    "dialect": "postgres"
  },
  "production": {
    "username": "root",
    "password": "password",
    "database": "omuk",
    "host": "localhost",
    "dialect": "postgres",
    "logging": false
  }
}

 

문제는 ES6에서 json을 어떻게 불러오냐입니다. 거의 수시간에 걸친 구글링 끝에 json을 불러오는 방법을 찾았습니다. config 파일을 가져와서 Sequelize 인스턴스를 사용합니다.

 

import Sequelize from "sequelize";
import { readFile } from "fs/promises";
const env = process.env.NODE_ENV || "development";
const config = JSON.parse(
  await readFile(new URL("../config/config.json", import.meta.url))
)[env];

const sequelize = new Sequelize(
  config.database,
  config.username,
  config.password,
  config
);
const db = {};

db.Sequelize = Sequelize;
db.sequelize = sequelize;

Object.keys(db).forEach((modelName) => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

export default db;

fs 모듈의 프로미스 툴을 가져옵니다. 그 다음, await로 동기적으로 불러와 정상적으로 객체로 가져옵니다. 가져올 때는 URL 객체로 가져오며, 환경에 따라서 다른 설정을 불러오도록 합니다.

 

새롭게 Sequelize 인스턴스를 생성하고 여기에 설정을 넣습니다. 그리고 db 라는 객체에 넣어서 관리합니다.

 

모델 정의

 

유저 데이터가 중요하기 때문에 유저 모델 먼저 만들겠습니다. ES6의 class를 활용합니다.

 

import { Sequelize, DataTypes, Model } from "sequelize";

class User extends Model {
  static init(sequelize) {
    return super.init(
      {
        email: {
          type: DataTypes.STRING(40),
          allowNull: false,
          unique: true,
        },
        nickname: {
          type: DataTypes.STRING(15),
          allowNull: false,
        },
        password: {
          type: DataTypes.STRING(100),
          allowNull: true,
        },
        provider: {
          type: DataTypes.STRING(10),
          allowNull: false,
          defaultValue: "local",
        },
        snsId: {
          type: Sequelize.STRING(30),
          allowNull: true,
        },
      },
      {
        sequelize,
        timestamps: true,
        underscored: false,
        schema: "User",
        tableName: "users",
        modelName: "User",
        paranoid: true,
        charset: "utf8",
        collate: "utf8_general_ci",
      }
    );
  }
}

export default User;

 

정적 메서드 init으로 User 모델을 초기화한다. 각 컬럼에 대한 설정을 작성한 다음, 옵션을 추가로 작성합니다.

 

  • timestamps: true로 설정할 시 createdAt, updatedAt 칼럼을 자동으로 생성해줍니다.
  • underscored: true로 설정할 시 스네이크 형식으로 칼럼을 작성해줍니다.
  • schema: 이 모델이 정의될 스키마를 설정합니다.
  • tableName: 테이블 이름을 설정합니다.
  • modelName: 모델 이름을 설정합니다.
  • paranoid: deletedAt 칼럼을 생성할지 정합니다. true로 설정하면 값을 삭제하지 않고 deletedAt의 값을 수정하도록 하여 삭제하도록 합니다.

CRUD

가장 기본적인 사이트의 기능인 CRUD를 구현하기 위해서 API를 설정하겠습니다.

 

1. C: Create

먼저 /auth 에 들어오는 요청에 대해서 Create 기능을 구현해보고자 합니다. 그렇기 때문에 auth 라우터에서 코드를 작성합니다. 

 

일단 모델을 불러옵니다.

import User from "../models/user.js";

먼저 회원가입 API를 통해 Create를 해보겠습니다.

 

일단 REST API에서 POST 메서드를 사용합니다.

router.post("/signup", async(req, res, next) => {
	const { email, nickname, password } = req.body;
});

post로 /auth/signup로 들어오는 요청에 대해 처리합니다. 이때, 데이터베이스를 통해서 조회를 해야 하므로 동기적으로 처리할 수 있도록 합니다. 조회를 하는 과정이 가장 먼저 처리되어야 하기 때문입니다.

 

쿼리를 실행할 때 오류가 발생할 수 있으므로 try, catch 구문으로 작성하도록 합니다. 일단 try에서 쿼리를 실행합니다. 회원가입 하기 전에 이미 가입한 유저면 안되기 때문에, 기존에 가입한 유저가 있는지 검사해주도록 했습니다. Read에서 다시 언급하도록 하겠습니다.

 

router.post("/signup", async (req, res, next) => {
  const { email, nickname, password } = req.body;
  try {
    const exUser = await User.findOne({ where: { email } });
    if (exUser) {
      return res.redirect(304, "signup");
    }
    await User.create({
      email,
      nickname,
      password,
    });
    return res.redirect("/");
  } catch (err) {
  	// TODO
  }
});

이미 존재하는 유저면 signup으로 리다이렉트 해줍니다. 원래 리소스의 현재 상태와 충돌이 발생하여 요청을 처리할 수 없을 때에는 409 코드를 사용해야 하지만, 일단 리다이렉트로 임시적으로 HTTP 상태코드 304 Not Modified를 사용했습니다.

 

존재하지 않는 유저라면, create를 통해서 값을 넣어줍니다. User에 email, nickname, password 컬럼을 만들었으므로 그대로 저렇게 값을 생성해준다. 만약 쿼리를 직접 작성한다면, IF NOT EXISTS INSERT INTO users(email, nickname, password) VALUES(email, nickname, password) 이런 식으로 작성하면 됩니다.

 

여기서 에러가 발생하면 catch로 넘어가기에 catch에도 내용을 넣어줍니다.

 

router.post("/signup", async (req, res, next) => {
  const { email, nickname, password } = req.body;
  try {
    const exUser = await User.findOne({ where: { email } });
    if (exUser) {
      return res.redirect(304, "signup");
    }
    await User.create({
      email,
      nickname,
      password,
    });
    return res.redirect("/");
  } catch (error) {
    console.error(error);
    return next(error);
  }
});

next로 넘겨줍니다.

 

postman이나 기타 툴을 통해서 실제로 요청을 보내보겠습니다. 일단 npm start로 서버를 실행합니다. 테스트를 위해 매번 데이터베이스를 초기화하고 싶어서 아래와 같이 연결을 처리했습니다.

db.sequelize
  .sync({ force: true })
  .then(() => {
    console.log("DB 연결 성공");
  })
  .catch((err) => {
    console.log("연결 실패", err);
  });

force 프로퍼티를 true로 설정해서 매번 초기화합니다. 짱짱

 

postman으로 요청을 보냅니다. POST 메서드로 설정한 다음 URI를 작성합니다. 그리고 HTTP 패킷 바디에 JSON으로 요청을 전송합니다.

 

위처럼 JSON으로 값을 전송한 후에 응답 바디를 보면

 

 

정상적으로 localhost:3000/ 으로 리다이렉트된 것을 볼 수 있습니다.

 

여기서 다시 동일한 요청바디로 요청을 보낸다면

/auth/signup 으로 get 요청이 발생한 것이 보입니다. 아직 get /auth/signup에 대해 작성하지 않아서 404 코드가 보입니다.

 

여기서 email의 설정으로 Not Null 을 설정했기 때문에 패킷 바디에서 email을 없애면 Server Error가 발생하기 때문에 이에 대해서도 수정이 필요합니다. 향후 보완하는 과정을 거치겠습니다.

2. R: Read

로그인 기능을 통해서 Read 를 구현합니다.

 

먼저 /auth/signin 패스에서 post 메서드 요청을 받습니다.

 

router.post("/signin", async (req, res, next) => {
	// TODO
});

이전과 동일한 이유로 데이터 베이스로 조회해야 하므로 async를 사용합니다.

 

그다음 body 값에서 email과 password를 받아와서 유저를 찾습니다. 그리고 findOne 메서드를 통해서 조회합니다.

 

router.post("/signin", async (req, res, next) => {
  const { email, password } = req.body;
  try {
    const exUser = await User.findOne({ where: { email, password } });
    if (!exUser) {
      return res.send({
        isSuccess: false,
        code: 410,
        message: "회원이 존재하지 않습니다.",
      });
    }

    return res.send({
      isSuccess: true,
      code: 200,
      message: "로그인 성공",
    });
  } catch (err) {
	// TODO
  }
});

findOne에서 where를 통해 조회할 값을 찾을 수 있도록 email과 password를 인수로 전달합니다. 그렇게 해서 값이 없다면 회원이 존재하지 않다는 응답을 전송합니다. 정상적으로 성공하면 성공 여부를 응답으로 보냅니다.

 

catch도 작성합니다.

 

router.post("/signin", async (req, res, next) => {
  const { email, password } = req.body;
  try {
    const exUser = await User.findOne({ where: { email, password } });
    if (!exUser) {
      return res.send({
        isSuccess: false,
        code: 410,
        message: "회원이 존재하지 않습니다.",
      });
    }

    return res.send({
      isSuccess: true,
      code: 200,
      message: "로그인 성공",
    });
  } catch (err) {
    console.error("DB 오류");
  }
});

 

이후에 postman으로 요청을 보냅니다.

 

그렇게 하여 아래처럼 응답이 오면 성공입니다.

 

 

3. U: Update

값을 업데이트 합니다.

 

일단 작성법을 연습해보는 것이라서 간단하게 라우트를 작성했습니다. 닉네임을 바꾸는 API로 테스트했습니다.

경로는 /auth/signup이며 메서드는 값을 일부 수정하기 때문에 PATCH 메서드를 사용했습니다.

 

router.route("/signup")
  .patch(async (req, res, next) => {
    try {
      const result = await User.update(
        {
          nickname: req.body.nickname,
        },
        { where: { email: req.body.email } }
      );
      res.send(result);
    } catch (err) {
      console.error(err);
      next(err);
    }
  });

 

업데이트 할 컬럼과 값을 첫 번째 객체의 프로퍼티로 넣어줍니다. 그 다음 두 번째 객체에는 where로 특정 값을 업데이트 할 수 있도록 이메일을 지정해주었습니다.

 

이제 Postman으로 검증해보겠습니다.

 

일단 POST /auth/signup 요청을 통해 회원가입하여 데이터베이스에 값을 넣겠습니다.

 

회원가입에 성공했고, createdAt과 updatedAt에 타임스탬프로 값이 저장되었습니다. 이제 PATCH /auth/signup 요청을 해보겠습니다. 현재는 간단한 구현을 위해 같은 경로에서 다른 메서드를 사용했는데, 향후에 경로의 파라미터를 읽어서 처리할 수 있도록 수정할 예정입니다.

 

pgAdmin을 통해서 테이블의 데이터를 조회한 결과 닉네임과 updatedAt의 데이터가 업데이트된 것을 볼 수 있습니다. 여기서의 문제점은 같은 닉네임을 입력하더라도 정상적으로 처리가 된다는 점입니다. 그래서 같은 닉네임을 사용하지 않도록 조정하는 과정이 필요해 보입니다.

 

4. D: DELETE

 

다음은 값 삭제입니다. 값 삭제는 아예 로우를 삭제하는 방법과, 제거됨을 저장할 컬럼을 만들어 실제로 값은 삭제하지 않는 방법이 있습니다. 여러 강의에서 실제로 제거하는 것보다 컬럼을 따로 두어서 삭제 여부를 저장해두는 것이 좋다고 하여 후자의 방법을 택했습니다. 그래서 애초에 모델을 정의할 때도 paranoid 옵션을 true로 정하였습니다.

 

라우트 메서드를 정의하겠습니다.

 

router.route("/signup")
  .delete(async (req, res, next) => {
    try {
      const result = await User.destroy({
        where: { email: req.body.email },
      });
      res.json(result);
    } catch (err) {
      console.error(err);
      next(err);
    }
  });

findOne 메서드와 같은 방법을 사용합니다. 이제 Postman으로 검증해보겠습니다.

 

 

성공적으로 회원가입이 되었으니 이제 삭제해보겠습니다. DELETE 메서드로 전환합니다.

성공적으로 회원 탈퇴가 완료됨을 확인할 수 있습니다.

 

아직 모든 과정이 정확하지 않습니다. 여러 가지 예외도 고려해야 하기 때문입니다.