[ MongoDB | pymongo | Javascript | TIL ] 댓글 수정, 삭제 API를 위한 고유값 생성

Haksoo JI·2022년 11월 24일
0

[ TIL ]

목록 보기
3/30

ObjectId를 활용한 댓글 수정, 삭제 API를 위한 고유값 생성

팀 소개 페이지를 만드는 미니 프로젝트 중에 우리는 페이지에 방명록 느낌으로 닉네임과 글을 남길 수 있는 기능을 넣었다. 클라이언트에서는 jQuery의 ajax를 사용해서 HTTP요청을 보내게 만들었고, 서버에서는 파이썬을 활용해서 flask서버와 MongoDB를 연결해 활용했다.

고유 Id(or Num) 이슈

우리는 단순히 댓글 등록만이 아니라 수정, 삭제 기능도 추가하려고 했다. 수정이나 삭제 기능을 넣을 때 대상 댓글을 확인할 수 있는 고유값을 각 댓글마다 가지고 있어야 한다. 웹개발종합반 강의에서는 댓글등록 기능만을 만들었기 때문에 고유값이 필요 없었다. 버킷리스트 강의에서 체크시 가로선이 그어지는 기능을 위해서 응용할 만한 방법을 알려주기는 했다. 바로 데이터베이스에서 존재하는 데이터 쌍의 개수를 세어서 1을 더한 숫자를 고유넘버로 입력하는 것이다.

count = list(db.bucket.find({},{'_id':False}))
num = len(count) + 1

vs

count = db.bucket.find({},{'_id':False}).count()
num = count + 1

위 두 코드 중에 하나를 사용해서 num값을 입력해주는 것이다. 그러나 이 방법에는 문제가 있다. num값이 데이터베이스의 데이터 갯수에 영향을 받기 때문에 중복사용되는 num값이 생길 수 있다.

예를 들어서, 5개의 댓글을 만들면 각 댓글의 num은 1, 2, 3, 4, 5일 것이다. 그 후에 만약 3번째 댓글을 삭제한다면 남은 댓글의 num은 1, 2, 4, 5가 된다. 여기서 새로운 댓글을 등록한다면 1, 2, 4, 5, 5가 각 댓글의 num값이 된다. 이럴 경우 각 댓글이 가지고 있는 버튼들의 이벤트 리스너가 제대로된 일을 하지 못하게 될 것이다. 5번째 댓글의 수정이나 삭제 버튼을 누르면 4번째 댓글이 수정되거나 삭제되는 식일 것이다.

그래서 우리는 이를 해결하고자 클라이언트에서 데이터를 GET할 때, MongoDB에서 자동 생성되어 저장되는 ObjectId값을 가져와서 고유 num으로 사용하자고 논의했다.

ObjectId 활용 실패

하지만 결국 ObjectId를 활용하는 방법은 실패로 돌아갔다. 이유는 여러가지가 있었다. 그리고 대부분 제대로 활용이 되지 않는 원인에 대해서 정확히 파악하지는 못하고 넘어가게 되었다. 그러기에는 프로젝트 마감기한까지 얼마 남지 않은 빠듯한 상태였기 때문이다.

새로 ID를 생성하는 것이 아니라, MongoDB에 저장시 자동생성되는 ObjectId를 활용하기위해 ObjectId를 불러오는 시도들을 해보았지만 실패해서 다음과 같이 해결했다.

서버 코드

# 댓글 작성 <- save_comment()
@app.route("/test", methods=["POST"])
def save_comment():
    name_receive = request.form['name_give']
    comment_receive = request.form['comment_give']
    num1 = random.randint(1, 255)
    num2 = random.randint(1, 255)
    num3 = random.randint(1, 255)

    doc = {
        'num1': num1,
        'num2': num2,
        'num3': num3,
        'name': name_receive,
        'comment': comment_receive
    }
    db.test.insert_one(doc)
    return jsonify({'msg': '댓글 등록!'})

# 댓글목록 조회 <- show_comment()
@app.route("/test", methods=["GET"])
def test_get():
    test_list = list(db.test.find({},{'_id':False}))
    return jsonify({'tests': test_list})

# 수정된 댓글 저장 <- save(num)
@app.route("/test/edit", methods=["POST"])
def save_edit():
    edit_receive = request.form['edit_give']
    num1_receive = request.form['num1_give']
    num2_receive = request.form['num2_give']
    num3_receive = request.form['num3_give']
    db.test.update_one({'num1': int(num1_receive), 'num2': int(num2_receive), 'num3': int(num3_receive)}, {'$set': {'comment': edit_receive}})
    return jsonify({'msg': '수정완료!'})

# 삭제 <- remove(num)
@app.route("/test/remove", methods=["POST"])
def remove():
    num1_receive = request.form['num1_give']
    num2_receive = request.form['num2_give']
    num3_receive = request.form['num3_give']
    db.test.delete_one({'num1': int(num1_receive), 'num2': int(num2_receive), 'num3': int(num3_receive)})
    return jsonify({'msg': '삭제!'})

수를 255까지로 제한한 이유는 그 이상의 숫자를 썼을 때 MongoDB에서 8bit 까지의 integer만 handling 할 수 있다는 에러메시지를 봤기 때문이다. 그래서 단순히 255개 숫자로는 댓글의 unique한 선택에 너무 중복이 많을 것 같아서 3개의 변수에 저장을 하도록 급하게 만들었다. 다행히 아직까지는 오류가 나타나지는 않았다.

클라이언트 코드

 $(document).ready(function () {
        show_comment();
      });
      function save_comment() {
        let name = $("#name").val();
        let comment = $("#comment").val();
        $.ajax({
          type: "POST",
          url: "/test",
          data: { name_give: name, comment_give: comment },
          success: function (response) {
            alert(response["msg"]);
            window.location.reload();
          },
        });
      }
      function show_comment() {
        $.ajax({
          type: "GET",
          url: "/test",
          data: {},
          success: function (response) {
            console.log("댓글목록조회", response["tests"]);
            let rows = response["tests"];
            for (let i = 0; i < rows.length; i++) {
              let name = rows[i]["name"];
              let comment = rows[i]["comment"];
              let num1 = rows[i]["num1"];
              let num2 = rows[i]["num2"];
              let num3 = rows[i]["num3"];
              console.log(typeof num1);
              console.log(num1);
              let temp_html = `<li class="card" id="li-${num1}-${num2}-${num3}">
            <div class="card-body">
              <blockquote class="blockquote mb-0">
                <p>${comment}</p>
                <footer class="from-who">${name}</footer>
              </blockquote>
            </div>
            <div class="comment-buttons">
              <button
               token interpolation">${num1}, ${num2}, ${num3})"
                type="button"
                class="btn btn-outline-primary"
              >
                수정
              </button>
              <button
               token interpolation">${num1}, ${num2}, ${num3})"
                type="button"
                class="btn btn-outline-primary"
              >
                삭제
              </button>
            </div>
          </li>`;
              $("#comment_list").append(temp_html);
            }
          },
        });
      }
      // 수정을 위한 input창 열기
      function addInput(num1, num2, num3) {
        console.log(num1);
        let li = $(`#li-${num1}-${num2}-${num3}`);
        console.log(li);
        let wanna_edit = $(`#li-${num1}-${num2}-${num3} p`).text();
        console.log(wanna_edit);
        li.empty();
        li.append($("<input/>", { type: "text", value: wanna_edit }));
        li.append($("<button >수정완료!</button>"));
        $(`#li-${num1}-${num2}-${num3} > button`).click(function () {
          save(num1, num2, num3);
        });
      }
      // 수정완료! 버튼 클릭시 이벤트리스너
      function save(num1, num2, num3) {
        let edit_done = $(
          `#li-${num1}-${num2}-${num3} > input[type=text]`
        ).val();
        console.log(num1, edit_done);
        $.ajax({
          type: "POST",
          url: "/test/edit",
          data: {
            edit_give: edit_done,
            num1_give: num1,
            num2_give: num2,
            num3_give: num3,
          },
          success: function (response) {
            alert(response["msg"]);
            window.location.reload();
          },
        });
      }
      function remove(num1, num2, num3) {
        $.ajax({
          type: "POST",
          url: "/test/remove",
          data: { num1_give: num1, num2_give: num2, num3_give: num3 },
          success: function (response) {
            alert(response["msg"]);
            window.location.reload();
          },
        });
      }

받은 숫자 3개를 이벤트리스너의 매개변수로 사용하고, HTML태그의 id값에 포함시켜서 jQuery로 접근하기 쉽게 만들었다.

추가) ObjectID 활용방법

프로젝트가 끝나고 천천히 하나씩 시도해서 결국 ObjectId를 활용할 수 있게 되어서 기록한다.

1. 에러메시지 해결: bson.errors.InvalidDocument: cannot encode object: <pymongo.cursor.Cursor object at 0x0000023F75B471C0>, of type: <class 'pymongo.cursor.Cursor'> ➡️ list() 사용

pymongo에서 num = db.buckets.find_one() 문법을 사용해서 ObjectId값을 불러오는 방법을 알 수 없었다. num = db.buckets.find_one({}), num = db.buckets.find_one({_id: True}) 등으로도 시도해봤지만 모두 같은 에러 메시지가 나타났다.

chrome 콘솔 에러: GET http://127.0.0.1:5000/bucket 500 (INTERNAL SERVER ERROR)
flask 서버 에러: bson.errors.InvalidDocument: cannot encode object: <pymongo.cursor.Cursor object at 0x0000023F75B471C0>, of type: <class 'pymongo.cursor.Cursor'>
  • POST http://127.0.0.1:5000/bucket 500 (INTERNAL SERVER ERROR)
    내부 서버 오류, 즉 서버에서 문제가 생겼다는 뜻인데 code가 500일 때는 구체적인 에러가 아니라 서버에서 발생하는 모든 에러를 총칭하는 것이다.
  • bson.errors.InvalidDocument: cannot encode object: <pymongo.cursor.Cursor object at 0x0000023F75B471C0>, of type: <class 'pymongo.cursor.Cursor'>
    이 메시지가 나타나는 이유는 find 메서드를 사용한 결과가 object가 아니라 cursor이기 때문이다.
    • 여기서 cursor란 db.<collection이름>.find()로 mongoDB에서 해당 collection의 documents를 찾을 때, 결과로써 리턴되는 documents의 위치를 가르키는 포인터(메모리주소를 저장하고 참조)이다.
    • cursor에 대해서는 이 블로그를 참조했다.

에러메시지를 통한 구글링으로 스택오버플로우에서 다음과 같은 내용을 찾았다. 참고하여 코드를 다음과 같이 바꿔보았다.

def bucket_get():
    bucket_list = list(db.buckets.find())
    return jsonify({'buckets': bucket_list})

여전히 오류메시지가 나타났지만, 서버에서의 메시지 내용이 달라졌다.

2. 에러메시지 해결: raise TypeError(f"Object of type {type(o).name} is not JSON serializable") ➡️ json.dumps 사용

TypeError: Object of type ObjectId is not JSON serializable
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
TypeError: Object of type ObjectId is not JSON serializable

이 메시지의 뜻은 ObjectId가 JSON 형식으로 변환되지 않았다는 것이다. 그래서 다음 블로그 내용을 참고해서 json 모듈을 import한 후, bucket_list를 문자열로 변환시켰다.


import json
def bucket_get():
    bucket_list = list(db.buckets.find({},{'_id': True}))
    json_bucket_list = json.dumps(str(bucket_list))
    return jsonify({'buckets': json_bucket_list})

더이상 서버와 크롬 콘솔에서 에러가 뜨지 않았다.

3. 문자열 한글깨짐 해결 ensure_ascii=False

response를 통해 받은 데이터 즉 jsonify({'buckets': json_bucket_list})를 클라이언트에서 response['buckets']로 받아 콘솔로 찍어보니까

"[
  {'_id': ObjectId('6375a14761306b2aa591118f'), 'bucket': '\ud14c\uc2a4\ud2b8 1', 'done': 0}, 
  {'_id': ObjectId('6375a17cb99b4210bfafda3d'), 'bucket': '\ud14c\uc2a4\ud2b82', 'done': 0}, 
  {'_id': ObjectId('6375a202d7968236725454f3'), 'bucket': '\ud14c\uc2a4\ud2b83', 'done': 0}, 
  {'_id': ObjectId('6375a261f732746654b41af4'), 'bucket': '\ud14c\uc2a4\ud2b85', 'done': 0}, 
  {'_id': ObjectId('637af2f1bb219b6a7b0d926a'), 'bucket': '1231231231', 'done': 0}, 
  {'_id': ObjectId('637b0b9f07c2a3f50434788b'), 'bucket': '123123', 'done': 0}, 
  {'_id': ObjectId('637b0dbcb2934c6018c549ac'), 'bucket': '1111', 'done': 0}
]"

처럼 나왔다.
보는바와 같이 한글 부분이 깨져서 나오는 것이 문제였다. 다행히 이 문제는 이 블로그를 참고하여 해결하였다.

# 서버측
def bucket_get():
    bucket_list = list(db.buckets.find())
    json_bucket_list = json.dumps(str(bucket_list), ensure_ascii=False)
    return jsonify({'buckets': json_bucket_list})
// 크롬 콘솔
"[
  {'_id': ObjectId('6375a14761306b2aa591118f'), 'bucket': '테스트 1', 'done': 0}, 
  {'_id': ObjectId('6375a17cb99b4210bfafda3d'), 'bucket': '테스트2', 'done': 0}, 
  {'_id': ObjectId('6375a202d7968236725454f3'), 'bucket': '테스트3', 'done': 0}, 
  {'_id': ObjectId('6375a261f732746654b41af4'), 'bucket': '테스트5', 'done': 0}, 
  {'_id': ObjectId('637af2f1bb219b6a7b0d926a'), 'bucket': '1231231231', 'done': 0}, 
  {'_id': ObjectId('637b0b9f07c2a3f50434788b'), 'bucket': '123123', 'done': 0}, 
  {'_id': ObjectId('637b0dbcb2934c6018c549ac'), 'bucket': '1111', 'done': 0}
]"

이제 받은 데이터들을 변수에 넣어서 다음과 같이 분배해보았다.

success: function (response) {
                    let rows = response['buckets']
                    for (let i = 0; i < rows.length; i++) {
                        let num = rows[i]['_id']
                        let bucket = rows[i]['bucket']
                        let done = rows[i]['done']
                        console.log(`${i}번째`, '_id:', num, 'typeof:', typeof num, `bucket:`, bucket)
                        let temp_html = `<li>
                                            <h2>✅ ${bucket}</h2>
                                            <buttontoken interpolation">${num})" type="button" class="btn btn-outline-primary">수정!</button>
                                            <buttontoken interpolation">${num})" type="button" class="btn btn-outline-primary">삭제!</button>
                                        </li>`
                        

다음은 결과화면이다.

뭔가 크게 잘못 되었다는 것을 알 수 있었다. 중간 중간 rows의 인덱스별로 콘솔을 찍어보고 깨달은 것은 JSON 모양처럼 생긴 저 데이터는 JSON형식이 아니라 모양만 비슷한 문자열이라는 것이었다.

4. 클라이언트에서 문자열을 JSON으로 변형 ➡️ replaceAll() 메서드 활용

⚠️ 어느 블로그를 통해 JSON.parse로 한번에 JSON형식으로 변환할 수 있나 시도해봤지만 작동하지 않았다. 잘못된 정보였다.

'_id'부분과 다른 데이터를 분리해서 불러오려고 해도 문제였다. db.buckets.find()를 사용해서 전체를 불러와서 json.dumps()로 클라이언트에서 확인해보면 위에처럼 {'_id': ObjectId('637b0dbcb2934c6018c549ac'), 'bucket': '1111', 'done': 0} 식으로 오브젝트 아이디가 불러와지지만, db.buckets.find({'_id': True}) 를 사용하면 다른 데이터들은 잘 나오는데 오브젝트 아이디만 빈 문자열이 반환된다.

이 문제는 원인을 파악하기에는 시간이 많이 걸릴 것 같아서 우선 넘어갔다. 개인적으로는 JSON.parse가 작동하지 않는 이유와 id부분만 빈 문자열로 반환되는 이유가 모두 ObjectId('xxxxxxxxxx') 형식으로 id값이 저장되어 있기 때문이라고 생각한다. 자료형 자체가 ObjectId라는 메서드를 해석할 수 있는 기능이 없다면 handling 할 수 없는 것이 당연한 것 같았다. 근데 뭐 그래서 어떻게 해야 나는 저 데이터를 받을 수 있는 것일까....

replaceAll() 메서드 활용

이후 생각해 본 방법은 문자열을 여러가지 메서드를 활용해서 데이터로 만드는 것이었다. ObjectId 부분이 문제라면 그 부분을 없에고 JSON.parse() 시도해보고자 했다. 몇 번의 삽질을 하다가 알게된 것은 JSON.parse() 메서드 자체가 특정 서식으로만 실행이 되도록 설계되어있었다. 각 key와 value를 감싸는 따옴표는 반드시 ""쌍따옴표를 써야하고 메서드에 넣기 위해서 string 형식으로 만들 때는 '' 으로 감싸야 했다. 그래서 실험용으로 코드를 짜서 테스트해보았다.

@app.route("/bucket", methods=["GET"])
def bucket_get():
    id_list = json.dumps(str(list(db.buckets.find())), ensure_ascii=False)
    return jsonify({'ids': id_list})

서버에서 이렇게 넘긴 후에

success: function (response) {
                    let rows = response['buckets']
                    console.log(response['ids'])
                    let rows_ids = response['ids'].replaceAll('ObjectId(','')
                                                  .replaceAll(')','')
                                                  .replaceAll('\"','')
                                                  .replaceAll('\'','"')
                    rows_ids = JSON.parse(`${rows_ids}`)
                    console.log(rows_ids)

클라이언트에서 이렇게 받아서 콘솔로 찍어보았더니....

드디어 원하는 모양이 나왔다. 감개무량하다.

이제 저 문자열을 활용해서 이벤트리스너의 매개변수 등으로 활용하면 된다.

5. 매개변수 사용 불가 ➡️ value값으로 할당

for (let i = 0; i < rows.length; i++) {
    let num = rows_ids[i]['_id']
	let bucket = rows_ids[i]['bucket']
    let temp_html = `<li>
						<h2>✅ ${bucket}</h2>
						<buttontoken interpolation">${num})" type="button" class="btn btn-outline-primary">수정!</button>
						<buttontoken interpolation">${num})" type="button" class="btn btn-outline-primary">삭제!</button>
					</li>`

각 버튼의 이벤트리스너를 저런식으로 만들어서 실행해보았다.

하지만 이런 에러코드가 나타났다. 아무래도 매개변수 타입을 받지 못하는 것 같았다. 매개변수로 넣는 것은 무리인 것 같아서 버튼의 value 값으로 집어넣은 후에 가져오는 것이 나을 것 같다는 생각이 들었다.

function edit_post() {

            const num = event.currentTarget.value
            console.log('edit_post 리스너입니다', num)

             $.ajax({
                type: "POST",
                url: "/bucket/edit",
                data: {num_give: num},
                success: function (response) {
                    console.log(response['bucket'])
                }
             });

6. 서버에서 ObjectId 모듈 임포트하기

클라이언트에서 이렇게 받아서 서버에 보내준 후, pymongo를 통해 MongoDB에서 id값을 조회한다. 그러기 위해서는 ObjectId 임포트해줘야 한다.(참고)

from bson.objectid import ObjectId

@app.route("/bucket/edit", methods=["POST"])
def bucket_edit():
    num_receive = request.form['num_give']
    obj_id = ObjectId(num_receive)
    bucket = db.buckets.find_one({'_id': obj_id}, {'_id': False})
    return jsonify({'msg': '버킷리스트 수정!', 'bucket':bucket})

이런식으로 하면 ObjectID를 통해 DB의 데이터를 찾고, 수정하고, 삭제할 수 있다.

profile
아직 씨앗입니다. 무슨 나무가 될까요?

0개의 댓글