깊은바다거북
개발 공부 기록
깊은바다거북
전체 방문자
오늘
어제
  • 분류 전체보기 (219)
    • JAVA (9)
    • JavaScript (15)
    • 스파르타코딩클럽 (11)
      • [내일배움단] 웹개발 종합반 개발일지 (5)
      • [내일배움캠프] 프로젝트와 트러블 슈팅 (6)
    • SQL | NoSQL (4)
    • CS 등등 (0)
    • TIL | WIL (173)
    • 기타 에러 해결 (3)
    • 내 살 길 궁리 (4)

인기 글

최근 글

최근 댓글

태그

  • Linked List
  • TIT (Today I Troubleshot)
  • 코딩테스트 연습문제
  • 01. 미니 프로젝트
  • 트러블 슈팅 Troubleshooting
  • TypeScript
  • Inorder Traversal(중위 순회)
  • 점화식(Recurrence Relation)
  • leetcode-cli
  • 최소 힙(Min Heap)
  • 혼자 공부하는 자바스크립트
  • POST / GET 요청
  • 프로그래머스
  • Til
  • BST(이진 탐색 트리)
  • 자바스크립트 기초 문법
  • DFS(깊이우선탐색)
  • Backtracking(백트래킹)
  • 자잘한 에러 해결
  • 재귀 함수
  • Leetcode
  • Binary Tree(이진 트리)
  • 최대 힙(Max Heap)
  • tree
  • Preorder Traversal(전위 순회)
  • BFS(너비우선탐색)
  • Trie
  • 시간 복잡도
  • 팀 프로젝트
  • 자료 구조
hELLO · Designed By 정상우.
깊은바다거북

개발 공부 기록

MongoDB의 ObjectId 최대한 간단하게 처리해서 프론트로 넘겨주기 ✔️
스파르타코딩클럽/[내일배움캠프] 프로젝트와 트러블 슈팅

MongoDB의 ObjectId 최대한 간단하게 처리해서 프론트로 넘겨주기 ✔️

2022. 12. 4. 21:00

아무리 정리하려고 해도 이상하게 매끄럽게 한 두 문장으로 문제 상황과 해결이 정리가 안 된다… 감안하고 봐주시길.


배경 스토리:

MongoDB의 데이터를 프론트로 보내주려고 할 때, 발목을 잡는 것이 MongoDB 고유 객체인 ObjectId이다. 이걸 그대로 프론트로 실어 보내려고 하면 에러가 난다. 그래서 사용하는 게 json_util.dupms()로 ObjectId를 한 번 파이썬이 인식 가능한 형태로 풀어주는 것이다. 그러면 ObjectId는 풀어지는데 다른 멀쩡한 문자까지 깨지게 되어, 그걸 또다시 정상적으로 바꿔주기 위해 json.loads()를 써주고 마지막으로 포장하는 게 jsonify(). 이렇게 3단계를 거쳐서 프론트로 보내주고 있었다.

GET 요청의 응답으로 Response객체가 아닌 json_util.dumps(코멘트 리스트)를 프론트로 넘겨줄 때,

  1. 문자가 "name": "\ub450\uadfc\ub450\uadfc” 등과 같이 깨져서 나오는 인코딩 에러와
  1. 프론트에서 값들을 출력했을 때 무수한 undefined가 찍히는 에러.

그리고 왜 json.loads(아까 바이너리로 읽히던 거)하면 제대로 되는 건지….

결론부터 얘기하면, json_util.dumps의 속성 중 ensure_ascii=False 처리해주면 문자가 깨져나오는 상황을 방지할 수 있다. 거기에 Ajax 쪽의 dataType 옵션을 사용해주면 된다.

무수한 undefined의 향연. 3079개인가 그랬다.
저 undefined들을 만들어낸 index.html의 코드

문제 상황 코드:

@app.route("/api/comments", methods=["GET"])
def get_comments():
		comments_list= list(db.miniProject.find())
    # resp = json_util.dumps(comments_list)
        # => 그냥 이렇게 해서는 바이너리로 깨져서 가는데다 어째선지 모든 값이 undefined로만 인식된다.
        # => 그래서 이전에 만들어둔 parse_json() 사용!
		resp = jsonify({'comments_list': parse_json(comments_list)})
    return resp

# parse ObjectId to JSON
def parse_json(data):
    return json.loads(json_util.dumps(data))

핵심 함수들:

#1. bson.json_util def dumps(obj: Any, *args: Any, **kwargs: Any) -> str

#2. json.loads()

Deserialize s (a str, bytes or bytearray instance containing a JSON document) to a Python object.

#3. flask.json def jsonify(*args: Any, **kwargs: Any) -> Response

지금 이 세 함수를 차례로 거쳐서 DB에서 뽑아낸 데이터 리스트를 프론트에 전달하고 있다.

그 과정을 파헤쳐보려고 한다.


먼저 서버의 GET 함수 안에서 json_util.dumps() → json.loads() → jsonify() 각각의 단계에서 타입과 값을 출력해보았다:

from bson import json_util
import json
from flask import jsonify

@app.route("/api/comments", methods=["GET"])
def get_comments():
		# 0단계: mongoDB에서 데이터 읽어와 파이썬 리스트로 저장
		comments_list = list(db.miniProject.find())

		# 1단계: json_util.dumps()
		resp = json_util.dumps(comments_list)
    print(type(resp),resp) # <class 'str'> [{"_id": {"$oid": "63734f437fee072e5d031a6c"}, "name": "\ub450\uadfc\ub450\uadfc", "comment": "\uccab \ub313\uae00"}, {"_id":... }, ... ]
    print()

		# 2단계: json.loads()
		resp = json.loads(resp)
    print(type(resp),resp) # <class 'list'> [{'_id': {'$oid': '63734f437fee072e5d031a6c'}, 'name': '두근두근', 'comment': '첫 댓글'}, {'_id':... }, ... ]
    print()

		# 3단계: jsonify()
		resp = jsonify({'comments_list': resp})
    print(type(resp),resp) # <class 'flask.wrappers.Response'> <Response 7167 bytes [200 OK]>
    print()

    return resp

0단계에서

는 mongoDB에서 찾은 데이터를 파이썬 리스트로 저장한다. 문제는 이 안에 ObjectId라는 파이썬이 알지 못하는 객체 타입이 들어있다는 것.

1단계에서

json_util.dumps()는 0단계의 파이썬 리스트를 받아서 그 안의 ObjectId 타입이었던 id값을 {"_id": {"$oid": "63734f437fee072e5d031a6c"}로 변환시켰다. 그리고 전체를 문자열로 감싸서 반환한다.

동영상에서는 이 상태 그대로 프론트에게 전달시켜버리는데, 여기서 내 프론트로 넘어오면 doc = response, doc[i][’comment’]로 출력하는 과정에서 무수한 undefined가 출력됐었다.

아! doc에 담기는 response가 문자열 그대로라서, doc[1]이라고 하면 그 긴 문자열 ‘[{"_id": {"$oid": "63734f437fee072e5d031a6c"}, "name": "\ub450\uadfc\ub450\uadfc", "comment": "\uccab \ub313\uae00"},…]’의 2번째 글자인 ‘{’(문자열)가 선택될 것이고, 거기서 [’comment’]라는 속성을 뽑아내려고 하니 undefined가 결과로 나왔던 것이다. doc[i][’comment’]로 의도한 것은 doc이 리스트일 것을 가정한 것이었다.

그러면 문자열로 온 response를 그대로 벗겨주기만 할 수는 없나? 자바스크립트쪽에서 그렇게 할 수도 있을 것 같은데, 문제는 또 있다. 바로 어째선지 한글이 다 유니코드 (바이트코드?)로 인코딩되어있다는 점이다.

⇒ 1단계(dumps())에서 문자열 깨짐 해결하기:

원래는 여기https://stackoverflow.com/questions/18337407/saving-utf-8-texts-with-json-dumps-as-utf-8-not-as-a-u-escape-sequence에 나온 것처럼 encode()/decode()까지 쓰려고 했는데, ensure_ascii=False 하나만으로도 해결이 되었다.

⇒ (.dumps()의 결과가 이미 문자열이라서 괜찮은 거라고 한다: “In 3.x, setting ensure_ascii=False is sufficient because the result from .dump or .dumps is already a string”)

# 1단계
resp = json_util.dumps(comments_list, ensure_ascii=False)
print(type(resp),resp) 
# <class 'str'> [{"_id": {"$oid": "63734f437fee072e5d031a6c"}, "name": "두근두근", "comment": "첫 댓글"}, {"_id":... }] => 잘 나온다!

2단계에서는

json.loads()가 그냥 문자열이었던 전의 결과값을 다시 파이썬 리스트로 변환해준다. 1단계에서 문자 깨진 상태 그대로 집어넣어도 똑같이 잘 만들어 반환해준다.

파이썬이나 자바스크립트에서 문자열⇒리스트 바로 변환 가능한지 찾다가 마주친 이미지. 너무 귀여워서… (출처: https://www.digitalocean.com/community/tutorials/python-convert-string-to-list)

(파이썬이나 자바스크립트에서 “[원래 리스트였다]”를 [원래 리스트였다]처럼 문자열→리스트로 변환하는 방법을 찾아보았는데 쉽게 찾을 수 없었다. 이미 json.loads()라는 답이 있는 상황이므로 빠르게 포기)

3단계는 생략이 가능하다.

2단계에서 이미 리스트로 만들었으므로 이를 그대로 전달해주면 프론트에서 for루프를 돌릴 수 있다 :

# app.py
@app.route("/api/comments", methods=["GET"])
def get_comments():
		# 0단계:
    comments_list = list(db.miniProject.find())

		# 1단계: 
    resp = json_util.dumps(comments_list, ensure_ascii=False)

		# 2단계:
    resp = json.loads(resp) # <class 'list'> [{'_id': {'$oid': '63734f437fee072e5d031a6c'}, 'name': '두근두근', 'comment': '첫 댓글'}, ...]

    return resp 


# index.html
let doc = [];
function reload_comment() {
    $.ajax({
        type: "GET",
        url: "/api/comments",
        data: {},
        success: function (response) {
            doc = response
            for (let i = 0; i < doc.length; i++) {
                let comment = doc[i]['comment']
                let name = doc[i]['name']
								...
						}
				}
		})
}

jsonify()로 굳이 Response객체로 만들어 전해주지 않아도 되는구나?!

⇒ Ajax가 response로 받을 수 있는 타입: 세상에 이런 귀한 정보를 찾았다..! https://stackoverflow.com/questions/15671679/jquery-ajax-response-type

ajax 안에 dataType: “json”이라고 키-값을 넣어주기만 하면 문자열로 response가 넘어와도 해결 가능하다고..!

밥 먹고 와서 시도해본다.

된다.

결론:

1단계 (json_util.dumps)에서 끝내버릴 수 있게 되었다!

from bson import json_util

# app.py
@app.route("/api/comments", methods=["GET"])
def get_comments():
    comments_list = list(db.miniProject.find())
    resp = json_util.dumps(comments_list, ensure_ascii=False)
    return resp 


# index.html
let doc = [];
function reload_comment() {
    $.ajax({
        type: "GET",
        url: "/api/comments",
        data: {}, 
				dataType: "json", 
        success: function (response) {
            doc = response
            for (let i = 0; i < doc.length; i++) {
                let comment = doc[i]['comment']
                let name = doc[i]['name']
								...
						}
				}
		})
}


Ajax의 dataType 종류

: dataType은 한마디로 내가 서버로부터 받는 응답을 무슨 타입으로 해석할 것이냐를 지정해줄 수 있는 옵션이다. text, html, xml, json, jsonp, 그리고 script가 가능하다.

// 예시 Ajax 요청
$.ajax({
    type: 'DELETE',
    url: '/api/comments/delete/' + _id,
    data: {},
		dataType: 'json'
    success: function (response) {
        alert(response)
        window.location.reload()
    }
})

Ajax 콜이 이렇게 있을 때, 서버에서 응답이 성공적으로 오면 JQuery가 “음 이건 xml 문서군”같은 처리를 먼저 해서 success handler에게 첫 번째 인자로 넘겨준다. success handler는 저기 success시에 호출하라고 붙어있는 콜백 함수이다. 한 마디로 서버로부터 받은 응답을 JQuery가 일차로 해독해서 넘겨준 게 저 “response”가 되는 것이다.

먼저는 서버로부터 오는 응답의 내재 타입을 기반으로 JQuery가 추측한다. 예를 들면 서버에서 jsonify()가 만들어 보내는 Response 객체가 application/json MIME 타입을 가지고 오면 아 얘는 JavaScript의 객체 타입으로 만들어야겠군 하고 변환한다. XML MIME 타입이었다면 XML형식으로 변환하고, 1.4 script였다면 script로(?), 그 외에 나머지 타입들은 다 (html 타입도) 문자열로 변환하게 된다.

여기까지는 내가 $.ajax() 콜을 할 때 dataType을 넘겨주지 않은 경우(=None인 경우)에 JQuery가 ‘똑똑하게 추측하는’ 것이고, 만약 dataType을 명시해주게 되면 서버로부터 오는 응답의 Content-Type header(아마 이게 MIME 타입?)은 무시된다.

  • “xml”: JQuery가 다룰 수 있는 XML 타입으로 해독하여 전달
  • “html”, “text” : 별도의 처리를 하지 않고 순수 문자열 타입으로 곧바로 전달
  • “script” : JavaScript로 해석하고 success handler에게 넘겨주기 전에 코드를 실행, 전달할 땐 순수 문자열 타입으로 전달
  • “json” : JSON으로 해석하여 JavaSciprt 객체를 전달. 이 옵션을 선택하려면 서버에서 응답을 보낼 때 오탈자 하나도 내면 안된다. JQuery 1.9부터는 빈 응답도 안되고 null이나 {}로 보내줘야 한다.
  • “jsonp” : JSON 블록을 JSONP를 이용해 적재한다고… 콜백을 덧붙일 수 있고 캐싱 여부도 지시할 수 있다고 한다.
  • “text xml” “jsonp text xml” 등 : 예를 들면 “jsonp text xml”은 JSONP 요청을 보내서, 순수 문자열로 응답을 받고, 그걸 JQuery가 XML로 해석하도록 지정할 수 있다. “text xml”같은 경우는 순수 문자열로 받은 응답을 XML로 해석하라고 지정한다고 한다. ⇒ 근데 어차피 “xml”이라고만 해도 내가 받은 응답의 내재 타입을 무시하고 XML로 해석해달라고 하는 건데, 이게 무슨 차이일까? 설명이 미흡하다.

method(나 type) 옵션은 디폴트로는 GET으로 항상 해석되는데, POST기능의 요청이 필요한 경우 type=”POST”로 지정해주면 된다. 그러면 data=””의 내용물이 항상 UTF-8 문자들로 전송된다고 한다.

  • 그러면 GET 요청이나 DELETE 등으로 보낼 때는 data의 내용이 다른 걸로 전송된다는 점에서 다르다는 걸까?

data 옵션은 쿼리 문자열 key1=value1&key2=value2 이나 객체 형식 {key1: 'value1', key2: 'value2'} 으로 써줄 수 있다. 객체 형식으로 주어진 데이터는 다시 쿼리 문자열로 변환되어 보내진다. 이렇게 문자열로 자동 변형되는 걸 막고 싶다면 processData 옵션을 false로 지정해주면 된다고 한다.

type 옵션은 1.9 이전의 옵션이고, 이후로는 method=”POST”같이 method 옵션을 사용한다고한다.

+ 나중에 다시 볼 만한 내용이 많다.

예를 들면 콜백 함수가 들어갈 수 있는 옵션들, 그 콜백 함수가 호출되는 순서, promise 콜백들로써 .done, .fail 등. 그리고 content-type이 들어있고 요청이 갈 때도 응답이 올 때도 해당되는 것 같은 header는 정확히 무엇인지, jsonp는 뭐라는지, Ajax 문서 어디에나 등장하는 것 같은 jqXHR객체가 뭔지.


참고:

https://stackoverflow.com/questions/15671679/jquery-ajax-response-type - String 타입 응답을 json으로 받는 방법

https://api.jquery.com/jquery.ajax/ - Ajax 공식 문서. “Data Types”와 “Sending Data to the Server” 꼭지를 읽어보면 도움된다.


Uploaded by N2T

    '스파르타코딩클럽/[내일배움캠프] 프로젝트와 트러블 슈팅' 카테고리의 다른 글
    • jsonify()는 뭘 어떻게 포장해서 프론트로 보내는가 ✔️
    • 비번 확인해서 삭제시키기 + 실패시 에러 메세지 띄우기 ✔️
    • HTML 스크립트 태그 내에서 제이쿼리 이벤트가 무시되는 에러 해결 ✔️
    • 01. 미니 프로젝트 - 팀 페이지를 만들 때
    깊은바다거북
    깊은바다거북

    티스토리툴바