RISK IT

[TIL19_23.1.27.] [Node] Express - 'westagram' feature/CRUD 코드 수정 본문

IT/TIL

[TIL19_23.1.27.] [Node] Express - 'westagram' feature/CRUD 코드 수정

nomoremystery 2023. 1. 27. 20:41
반응형

1. 작업 내용

TIL15에 올렸던 feature/CRUD를 추가한 후 멘토님들의 리뷰를 받아 지속적으로 수정해왔다.

  • RESTful한 엔드포인트로의 수정
  • 가독성 향상을 위해 SQL query문 수정
  • async, await을 활용한 비동기 처리(기존 callback과의 혼합된 방식에서 변경)
  • likes table UNIQUE 제약조건 추가
  • '?'를 활용한 데이터 매핑

2. 소스코드

⌨️ 기존 소스코드

require('dotenv').config();

const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const { DataSource } = require('typeorm');

const mysqlDataSource = new DataSource({
  type: process.env.TYPEORM_CONNECTION,
  host: process.env.TYPEORM_HOST,
  port: process.env.TYPEORM_PORT,
  username: process.env.TYPEORM_USERNAME,
  password: process.env.TYPEORM_PASSWORD,
  database: process.env.TYPEORM_DATABASE,
});

mysqlDataSource
  .initialize()
  .then(() => {
    console.log('Data Source has been initialized!');
  })
  .catch((err) => {
    console.error('Error during Data Source initialization', err);
    mysqlDataSource.destroy();
  });

const app = express();

app.use(cors());
app.use(morgan('dev'));
app.use(express.json());

// health check
app.get('/ping', (req, res) => {
  res.status(200).json({ message: 'pong' });
});

app.post('/users/signup', async (req, res) => {
  const { name, email, password, profileImage } = req.body;

  await mysqlDataSource.query(
    `INSERT INTO users (
      name, 
      email, 
      password, 
      profile_image
    )
      VALUES (
        ?, 
        ?, 
        ?, 
        ?
      );
    `,
    [name, email, password, profileImage]
  );

  res.status(201).json({ message: 'userCreated' });
});

app.post('/posts/users', async (req, res) => {
  const { title, content, postImgUrl, userId } = req.body;

  await mysqlDataSource.query(
    `INSERT INTO posts (
      title,
      content,
      post_image_url,
      user_id
      )
      VALUES (
        ?,
        ?,
        ?,
        ?
      );
        `,
    [title, content, postImgUrl, userId]
  );

  res.status(201).json({ message: 'postCreated' });
});

app.get('/posts/lookup', async (req, res) => {
  await mysqlDataSource.query(
    `SELECT
      u.id AS userId,
      u.profile_image AS userProfileImage,
      p.id AS postingId,
      p.post_image_url AS postingImageUrl,
      p.content AS postingContent
    FROM posts p
    INNER JOIN users u ON u.id = p.user_id;
    `,
    (err, data) => {
      res.status(200).json({ data });
    }
  );
});

app.get('/posts/lookup/:userId', async (req, res) => {
  const { userId } = req.params;

  await mysqlDataSource.query(
    `SELECT
    u.id AS userId,
    u.profile_image AS userProfileImage,
    pi.post_informations AS postings
    FROM
      users u
    INNER JOIN (
      SELECT
        user_id,
        JSON_ARRAYAGG(
          JSON_OBJECT (
            "postingId", id,
            "postingImageUrl", post_image_url,
            "postingContent", content
          )
        ) AS post_informations
      FROM
        posts
      GROUP BY
        user_id
    ) pi 
      ON pi.user_id = u.id
    WHERE
      u.id = ${userId};
    `,
    (err, data) => {
      res.status(200).json({ data });
    }
  );
});

app.patch('/posts/update/:userId/:postId', async (req, res) => {
  const { userId, postId } = req.params;
  const { content } = req.body;
  await mysqlDataSource.query(
    `UPDATE posts
    SET
      content = ?
    WHERE 
      user_id = ${userId} AND id = ${postId}
    `,
    [content]
  );

  await mysqlDataSource.query(
    `SELECT
      u.id AS userId,
      u.profile_image AS userProfileImage,
      p.id AS postingId,
      p.post_image_url AS postingImageUrl,
      p.content AS postingContent
    FROM posts p
    INNER JOIN users u ON u.id = p.user_id
    WHERE u.id=${userId} AND p.id=${postId}
    `,
    (err, data) => {
      res.status(201).json({ data });
    }
  );
});

app.delete('/posts/delete/:postId', async (req, res) => {
  const { postId } = req.params;

  await mysqlDataSource.query(
    `DELETE
    FROM posts
    WHERE posts.id = ${postId}
    `
  );

  res.status(200).json({ message: 'postingDeleted' });
});

app.post('/likes/:userId/:postId', async (req, res) => {
  const { userId, postId } = req.params;

  await mysqlDataSource.query(
    `INSERT INTO likes (
      user_id,
      post_id
    )
      VALUES (
        ?, 
        ?
        );
    `,
    [userId, postId]
  );

  res.status(201).json({ message: 'likeCreated' });
});

const PORT = process.env.PORT;
const start = async () => {
  try {
    app.listen(PORT, () => console.log(`Server is listening on ${PORT}!!`));
  } catch (err) {
    console.error(err);
  }
};

start();

👨‍💻 개선된 소스코드

require('dotenv').config();

const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const { DataSource } = require('typeorm');

const mysqlDataSource = new DataSource({
  type: process.env.TYPEORM_CONNECTION,
  host: process.env.TYPEORM_HOST,
  port: process.env.TYPEORM_PORT,
  username: process.env.TYPEORM_USERNAME,
  password: process.env.TYPEORM_PASSWORD,
  database: process.env.TYPEORM_DATABASE,
});

mysqlDataSource
  .initialize()
  .then(() => {
    console.log('Data Source has been initialized!');
  })
  .catch((err) => {
    console.error('Error during Data Source initialization', err);
    mysqlDataSource.destroy();
  });

const app = express();

app.use(cors());
app.use(morgan('dev'));
app.use(express.json());

// health check
app.get('/ping', (req, res) => {
  res.status(200).json({ message: 'pong' });
});

app.post('/users/signup', async (req, res) => {
  const { name, email, password, profileImage } = req.body;

  await mysqlDataSource.query(
    `INSERT INTO users (
      name, 
      email, 
      password, 
      profile_image
    )
      VALUES (
        ?, 
        ?, 
        ?, 
        ?
      );
    `,
    [name, email, password, profileImage]
  );

  return res.status(201).json({ message: 'userCreated' });
});

app.post('/posts', async (req, res) => {
  const { title, content, postImgUrl, userId } = req.body;

  await mysqlDataSource.query(
    `INSERT INTO posts (
      title,
      content,
      post_image_url,
      user_id
      )
      VALUES (
        ?,
        ?,
        ?,
        ?
      );
        `,
    [title, content, postImgUrl, userId]
  );

  return res.status(201).json({ message: 'postCreated' });
});

app.get('/posts', async (req, res) => {
  const posts = await mysqlDataSource.query(
    `SELECT
      u.id AS userId,
      u.profile_image AS userProfileImage,
      p.id AS postingId,
      p.post_image_url AS postingImageUrl,
      p.content AS postingContent
    FROM posts p
    INNER JOIN users u ON u.id = p.user_id;
    `
  );

  return res.status(200).json({ posts });
});

app.get('/posts/:userId', async (req, res) => {
  const { userId } = req.params;

  const [result] = await mysqlDataSource.query(
    `
    SELECT
      u.id AS userId,
      u.profile_image AS userProfileImage,
      pi.post_informations AS postings
    FROM users u
    INNER JOIN (
      SELECT
        user_id,
        JSON_ARRAYAGG(
          JSON_OBJECT (
            "postingId", id,
            "postingImageUrl", post_image_url,
            "postingContent", content
          )
        ) AS post_informations
      FROM posts
      GROUP BY user_id
    ) pi
      ON pi.user_id = u.id
    WHERE u.id = ?;
    `,
    [userId]
  );

  return res.status(200).json({ data: result });
});

app.patch('/posts/:postId/:userId', async (req, res) => {
  const { userId, postId } = req.params;
  const { content } = req.body;

  await mysqlDataSource.query(
    `UPDATE posts
    SET
      content = ?
    WHERE 
      user_id = ? AND id = ?
    `,
    [content, userId, postId]
  );

  const [result] = await mysqlDataSource.query(
    `
    SELECT
      u.id AS userId,
      u.profile_image AS userProfileImage,
      p.id AS postingId,
      p.post_image_url AS postingImageUrl,
      p.content AS postingContent
    FROM posts p
    INNER JOIN users u ON u.id = p.user_id
    WHERE u.id = ? AND p.id = ?
    `,
    [userId, postId]
  );

  return res.status(201).json({ data: result });
});

app.delete('/posts/:postId', async (req, res) => {
  const { postId } = req.params;

  await mysqlDataSource.query(
    `DELETE
    FROM posts
    WHERE posts.id = ?
    `,
    [postId]
  );

  return res.status(200).json({ message: 'postingDeleted' });
});

app.post('/likes/:userId/:postId', async (req, res) => {
  const { userId, postId } = req.params;

  await mysqlDataSource.query(
    `INSERT INTO likes (
      user_id,
      post_id
    )
      VALUES (
        ?, 
        ?
        );
    `,
    [userId, postId]
  );

  return res.status(201).json({ message: 'likeCreated' });
});

const PORT = process.env.PORT;
const start = async () => {
  try {
    app.listen(PORT, () => console.log(`Server is listening on ${PORT}!!`));
  } catch (err) {
    console.error(err);
  }
};

start();

3. 변경 부분 비교

RESTful한 엔드포인트

변경 전

app.post('/posts/users', async (req, res) => {

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

app.get('/posts/lookup/:userId', async (req, res) =>

app.patch('/posts/update/:userId/:postId', async (req, res) => {

app.delete('/posts/delete/:postId', async (req, res) => {

변경 후

app.post('/posts', async (req, res) => { // 1️⃣

app.get('/posts', async (req, res) => { // 2️⃣

app.get('/posts/:userId', async (req, res) => { // 3️⃣

app.patch('/posts/:postId/:userId', async (req, res) => { // 4️⃣

app.delete('/posts/:postId', async (req, res) => { // 5️⃣

1️⃣ 게시글 등록에 관한 API임으로, 두 개의 다른 리소스가 엔드포인트에 명시되어 혼선을 야기시키지 않도록 /posts 로 변경
2️⃣ get이라는 http 메소드에서 해당 엔드포인트가 행하고자 하는 동사적 기능이 충분히 설명되어 있기에 lookup이라는 불필요한 동사적 어구는 삭제
3️⃣ 2번과 마찬가지
4️⃣ 2번과 마찬가지로, update라는 의미가 http 메소드 .patch에 포함되어 있기에 삭제
5️⃣ 2번과 마찬가지로, delete라는 의미가 http 메소드 .delete에 포함되어 있기에 삭제

SQL 쿼리문 수정 및 '?'를 활용한 데이터 매핑

변경 전

`SELECT
    u.id AS userId,
    u.profile_image AS userProfileImage,
    pi.post_informations AS postings
    FROM
      users u
    INNER JOIN (
      SELECT
        user_id,
        JSON_ARRAYAGG(
          JSON_OBJECT (
            "postingId", id,
            "postingImageUrl", post_image_url,
            "postingContent", content
          )
        ) AS post_informations
      FROM
        posts
      GROUP BY
        user_id
    ) pi 
      ON pi.user_id = u.id
    WHERE
      u.id = ${userId};
    `;

변경 후

`
    SELECT
      u.id AS userId,
      u.profile_image AS userProfileImage,
      pi.post_informations AS postings
    FROM users u
    INNER JOIN (
      SELECT
        user_id,
        JSON_ARRAYAGG(
          JSON_OBJECT (
            "postingId", id,
            "postingImageUrl", post_image_url,
            "postingContent", content
          )
        ) AS post_informations
      FROM posts
      GROUP BY user_id
    ) pi
      ON pi.user_id = u.id
    WHERE u.id = ?;
    `,
  [userId];
  • SQL 쿼리문은 문법의 구조를 이해한다면 쉽게 가독성을 높일 수 있다.
  • SQL문에 변수를 직접 집어 넣는 대신, Mapper를 통해 SQL문과 변수를 분리시킴으로써 유지보수가 용이하도록 변경하였다.

다른 SQL문도 가독성 향상을 위해 코드를 변경하였지만, 동일 내용이기에 변경 전후 코드는 첨부하지 않았다.

async, await을 활용한 비동기 처리(기존 callback과의 혼합된 방식에서 변경)

변경 전

app.get('/posts/lookup', async (req, res) => {
  await mysqlDataSource.query(
    `SELECT
      u.id AS userId,
      u.profile_image AS userProfileImage,
      p.id AS postingId,
      p.post_image_url AS postingImageUrl,
      p.content AS postingContent
    FROM posts p
    INNER JOIN users u ON u.id = p.user_id;
    `,
    (err, data) => {
      res.status(200).json({ data });
    }
  );
});
  • 기존 코드에서 callback 방식의 비동기 처리와 async, await 방식의 비동기 처리가 혼합된 모습

변경 후

app.get('/posts', async (req, res) => {
  const posts = await mysqlDataSource.query(
    `SELECT
      u.id AS userId,
      u.profile_image AS userProfileImage,
      p.id AS postingId,
      p.post_image_url AS postingImageUrl,
      p.content AS postingContent
    FROM posts p
    INNER JOIN users u ON u.id = p.user_id;
    `
  );

  return res.status(200).json({ posts });
});
  • 수정 후 async, await을 활용한 비동기 처리의 코드의 모습.
  • return은 안써도 되지만, 아직 배우는 단계이기 때문에 명확하게 표현하고자 코드에 포함시켰다.

다른 요청사항들도 async, await만을 사용한 비동기 처리 방식으로 코드를 변경하였지만, 동일 내용이기에 변경 전후 코드는 첨부하지 않았다.

likes table UNIQUE 제약조건 추가

likes table의 UNIQUE 제약조건은 위의 코드에 없고 다른 파일에 있기 때문에 아래에 첨부

 -- migrate:up
CREATE TABLE
  likes (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    user_id int NOT NULL,
    post_id int NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    CONSTRAINT like_user_id_fkey FOREIGN KEY (user_id) REFERENCES users (id),
    CONSTRAINT like_post_id_fkey FOREIGN KEY (post_id) REFERENCES posts (id),
    CONSTRAINT likes_user_id_post_id_ukey UNIQUE (user_id, post_id)
  );

-- migrate:down
DROP TABLE
  likes;

user_id와 post_id에 함께 UNIQUE 제약조건을 걸어줌으로써, 한 유저가 하나의 포스트에 중복으로 좋아요를 누르지 못하도록 설정하였다.

반응형