아무리 정리하려고 해도 이상하게 매끄럽게 한 두 문장으로 문제 상황과 해결이 정리가 안 된다… 감안하고 봐주시길.
배경 스토리:
MongoDB의 데이터를 프론트로 보내주려고 할 때, 발목을 잡는 것이 MongoDB 고유 객체인 ObjectId이다. 이걸 그대로 프론트로 실어 보내려고 하면 에러가 난다. 그래서 사용하는 게 json_util.dupms()로 ObjectId를 한 번 파이썬이 인식 가능한 형태로 풀어주는 것이다. 그러면 ObjectId는 풀어지는데 다른 멀쩡한 문자까지 깨지게 되어, 그걸 또다시 정상적으로 바꿔주기 위해 json.loads()를 써주고 마지막으로 포장하는 게 jsonify(). 이렇게 3단계를 거쳐서 프론트로 보내주고 있었다.
GET 요청의 응답으로 Response객체가 아닌 json_util.dumps(코멘트 리스트)를 프론트로 넘겨줄 때,
- 문자가 "name": "\ub450\uadfc\ub450\uadfc” 등과 같이 깨져서 나오는 인코딩 에러와
- 프론트에서 값들을 출력했을 때 무수한 undefined가 찍히는 에러.
그리고 왜 json.loads(아까 바이너리로 읽히던 거)
하면 제대로 되는 건지….
결론부터 얘기하면, json_util.dumps의 속성 중 ensure_ascii=False 처리해주면 문자가 깨져나오는 상황을 방지할 수 있다. 거기에 Ajax 쪽의 dataType 옵션을 사용해주면 된다.
문제 상황 코드:
@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단계에서 문자 깨진 상태 그대로 집어넣어도 똑같이 잘 만들어 반환해준다.
(파이썬이나 자바스크립트에서 “[원래 리스트였다]”를 [원래 리스트였다]처럼 문자열→리스트로 변환하는 방법을 찾아보았는데 쉽게 찾을 수 없었다. 이미 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