full stack mysql, node js express,react

JS FULL STACK 6장 , Node.js express Todo앱 CRUD API 만들기

우주전사버즈 2024. 1. 18. 12:24

왜 공부?

지난 시간 Todo앱 DB 설계를 완성함. 해당 DB를 클라이언트(사용자)관점에서 이용할수있도록 사용자가

서버에 데이터를 요청(생성,읽기,수정,삭제)하면 해당 데이터에 대한 요청에 맞는 반환을 주도록  API를 만들기 위해

Node.js express 프레임워크를 사용한다고하였음

 

Todo API git hub

 

GitHub - MkBaek0229/express_Todo_2024-01

Contribute to MkBaek0229/express_Todo_2024-01 development by creating an account on GitHub.

github.com

 

만들기전 설치해준 라이브러리(npm) 개발환경셋팅

express :

웹 애플리케이션의 라우팅, 미들웨어, 요청 및 응답 처리 등을 쉽게 구현할수 있는 웹 프레임워크(js)

mysql2 :

Node.js 애플리케이션에서 MySQL 데이터베이스에 연결하고, 쿼리를 실행하고, 결과를 가져오는 등의 작업을 수행가능 mysql2는 Promises와 async/await 같은 비동기 패턴을 지원 , 콜백을 사용하여 데이터베이스 작업을 처리할 수도 있음

nodemon :

일반적 node.js 앱 개발시 코드가 변경되면 서버를 재시작해줘야하는 문제를 해결하기 위한 라이브러리 코드변경 동작을 자동으로감지해서 서버를 자동으로 재실행해줌

Cors :

HTML파일(클라이언트)에서 서버로 요청했을때 보안상의 문제로 차단되는 문제가 존재함 HTML의 요청에 응답이 잘되도록 해당 모듈을 설치해주어야한다.

 

JSON Viewer : 크롬 확장 플러그인

서버로부터 JSON 데이터를 받았을때 가독성좋고 이쁘게 정렬된 형태로 반환해줌

 

 

먼저 mysql2 라이브러리를 통해 node.js에서 내가만든 Todo DB와 연결

const pool = mysql.createPool({
  host: "localhost",
  user: "alsrl6678",
  password: "alsrl1004",
  database: "todo",
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
  # dateStrings를 true로 해주지않으면 생성날짜와같은 데이터를 받아올때 문제가생김
  dateStrings: true
});

 

Todo 데이터베이스 회원가입 기능 

// 회원가입 API
app.post("/signup", async (req, res) => {
  const { username, password, callnum } = req.body;

  // 필수 정보 검증
  if (!username || !password || !callnum) {
    res.status(400).json({
      resultCode: "F-1",
      msg: "사용자 이름, 비밀번호, 전화번호를 모두 입력해주세요",
    });
    return;
  }

  try {
    // 사용자 등록
    const [insertUserRs] = await pool.query(
      `
      INSERT INTO member (name, password, callnum) VALUES (?, ?, ?);
      `,
      [username, password, callnum]
    );

    res.json({
      resultCode: "S-1",
      msg: "회원가입이 완료되었습니다",
      data: {
        userId: insertUserRs.insertId,
        username,
      },
    });
  } catch (error) {
    console.error("에러 발생:", error);
    // 중복된 사용자 이름 또는 다른 오류에 대한 처리
    if (error.code === 'ER_DUP_ENTRY') {
      res.status(409).json({
        resultCode: "F-1",
        msg: "이미 존재하는 사용자 이름입니다",
      });
    } else {
      res.status(500).json({
        resultCode: "F-1",
        msg: "서버 에러",
      });
    }
  }
});

 

요구사항 : 사용자가 로그인이 된 상태에서만 Todo를 이용할 수 있도록 한다.

요구사항에맞게 회원가입을 하고 로그인을 햇을때만 Todo를 이용할수 있게 만드려고한다.

 

Todo 사용자 로그인 여부 확인 미들웨어

// 사용자 로그인 여부를 확인하는 미들웨어
const checkLogin = async (req, res, next) => {
  const { username } = req.params;

  try {
    // 사용자가 존재하는지 확인
    const [userRows] = await pool.query(
      `
      SELECT id FROM member WHERE name = ?;
      `,
      [username]
    );

    if (userRows.length === 0) {
      res.status(401).json({
        resultCode: "F-1",
        msg: "로그인이 필요합니다",
      });
      return;
    }

    next(); // 다음 미들웨어로 이동
  } catch (error) {
    console.error("에러 발생:", error);
    res.status(500).json({
      resultCode: "F-1",
      msg: "서버 에러",
    });
  }
};

// 사용자 로그인 체크 미들웨어를 관련된 라우트에 적용
app.use("/:username/todos", checkLogin);

미들웨어 : 요청,응답 처리되기전에 중간단계에서 실행되는 함수(중간단계 작업)

/:username/todos 경로에 대한 모든 요청에 대해 checkLogin 미들웨어를 적용한다 

== 

저 경로로 요청하려면 로그인여부를 먼저 확인해야됨 -> 없는 회원이면 이용을 못한다.

 

Todo 데이터베이스 데이터 모두 조회 (Create, Read ,Update , Delete) 

한명의 유저가 작성한 todos 목록을 조회하는것이 목적이였다.

get요청으로 주소창에서 :username이라는 이름을 가진 params(변수)를 입력받아 todos 주소로 데이터 요청받도록함

 

await pool.query를 사용해서 데이터베이스에서 쿼리를 실행하고 그 결과를 처리하도록하였음

sql join을 통해 사용자명과 할일목록을 보여주는형태로 query문 작성

// 유저가 작성한 데이터 조회
app.get("/:username/todos", async (req, res) => {
  const {username} = req.params;

  const [todosrows] = await pool.query(
  `
  SELECT member.name AS 회원이름, todo.contents AS 할일내역
  FROM member
  JOIN todo_member ON member.id = todo_member.member_id
  JOIN todo ON todo_member.todo_id = todo.id
  WHERE member.name = ?
  `,
  [username]
  );
  res.json({
    resultCode: "S-1",
    msg: "성공",
    data: todosrows,
  });
});

 

알게 된 것 

이전에는 res(반환)값으로 json데이터를 줄때 json데이터만 반환했었음 

근데 이것을 보다 명확하게 데이터요청에 성공했는지 실패하였는지에 대해 정확하게 알려줘야한다는 사실을 깨닫고 수정함

# 이전 반환(데이터만그냥 무지성으로돌려줌)
res.json({
    data: rows,
  });
  
# 바꾼 반환(데이터 요청 성공/실패 여부 메시지 추가)
res.json({
    resultCode: "S-1",
    msg: "성공",
    data: rows,
  });

 

Todo 데이터베이스 데이터 단건 조회 (Create, Read ,Update , Delete)

위에서는 한명의 회원이 작성한 모든 todo를 조회하였는데 하나의 todo만 조회하는 단건조회에 대한 요청도 처리해주기로 하였다

app.get("/:username/todos/:no", async (req, res) => {
  const { username , no} = req.params;
  const [todorows] = await pool.query(
    `
    SELECT member.name AS 회원이름, todo.contents AS 할일내역
    FROM member
    JOIN todo_member ON member.id = todo_member.member_id
    JOIN todo ON todo_member.todo_id = todo.id
    WHERE member.name = ?
    AND todo.id = ?
    `,
    [username , no]
    );

  if (todorows.length == 0) {
    res.status(404).json
    ({
      resultCode: "F-1",
      msg: "해당 번호로 작성된 todo가 없습니다.",
    });
    
    return;
  }

  res.json({
    resultCode: "S-1",
    msg: "성공",
    data: todorows,
  });
});

Todo 데이터베이스 데이터 삭제 (Create, Read ,Update , Delete)

특정 회원이 작성한 할일을 삭제할수있어야한다.

app.delete("/:username/todos/:no", async (req, res) => {
  const { username , no} = req.params;  

  const [todorows] = await pool.query(
    ` 
    DELETE FROM todo
    WHERE id = ?
      AND id IN (
        SELECT todo_id
        FROM todo_member
        WHERE member_id = (
          SELECT id
          FROM member
          WHERE name = ?
          )
      )
    `,
    [no,username]
  );
  if (todorows.length == 0) {
    res.status(400).json({
      resultCode: "F-1",
      msg: "해당 todo는 존재하지 않습니다",
    });
    return;
  }

  

  res.status(200).json({
    resultCode: "F-1",
    msg: `${no}번 할일을 삭제하였습니다`,
  });
});

새로 알게 된것

특정 회원의 특정 할일을 삭제하는것이 조금 어려웠다.

그냥 할일만 삭제하는거면 todo id만 입력받아 삭제하면되는데

내가만든 todo에서는 todo, member,그리고 그둘을 연결한 todo_member테이블도있으니깐

전부다건드려줘야되서 IN(서브쿼리)를 통해 얻은 결과목록중에 todo 테이블의 id와 일치하는 값을  찾아 삭제하도록해줘야했다.

const [todorows] = await pool.query(
    ` 
    DELETE FROM todo
    WHERE id = ?
      AND id IN (
        SELECT todo_id
        FROM todo_member
        WHERE member_id = (
          SELECT id
          FROM member
          WHERE name = ?
          )
      )
    `,
    [no,username]
  );

가장 안쪽의 서브쿼리를 해석

          SELECT id
          FROM member
          WHERE name = ?

member 테이블에서 특정 name을 가진 회원의 id를 가져온다고되있고

 

그다음 서브쿼리 해석

SELECT todo_id
        FROM todo_member
        WHERE member_id =

todo_member(매핑테이블)테이블 에서 특정 member_id값과 위쪽에서 얻은 회원테이블의 id와 일치한다면 해당 member_id의 todo_id를 얻어온다는뜻

그리고 해당 todo_id를 외부 쿼리에서 얻은 (DELETE FROM todo WHERE id = ?) id와 일치할때 삭제한다. 

 

 

SQL 먼가 어렵다?

기본적인 query문 요청에 의한 crud기능을 만드려고했는데 

내가만드는 Todo앱은 회원이 로그인 된 상태에서만 todo를 작성할수 있다라는 요구사항에 충족하려다보니

회원가입 API도만들게되고 로그인여부를 파악 API 만들다보니 미들웨어라는것도 알게되고

조금 복잡하게 느꼇고 JOIN이 뭔지는 아는데 실제로 적용시키려고하니깐 어떻게 사용해야될지 머리가 잘 안돌아가서

자료를 찾아보면서 적용해보고 gpt한테 코드를 검증해달라고했다..