저희 조는 노션의 데이터를 시각화한 맵 정보를 MongoDB에 저장하고 있습니다. MongoDB를 사용할 수 있게 만드는 자바스크립트 라이브러리로 Mongoose를 사용하고 있는데, Mongoose는 데이터 스키마를 지정해서 엄밀한 형태의 데이터를 저장할 수 있습니다. 문제는 일부 데이터를 Mongoose를 이용해 MongoDB에 저장하려고 할 때, 다음과 같은 오류가 뜬다는 것이었습니다.
pages.9.keywords: Cast to Map failed for value "{
[0] '객체': 15,
[0] prototype: 11,
[0] '표현': 7,
[0] constructor: 7,
[0] class: 7,
[0] plain: 5,
[0] text: 5,
[0] instance: 4,
[0] '객체표현': 4,
[0] pattern: 4,
[0] '클래스': 3,
[0] '참조': 3,
[0] '생성': 3,
[0] '프로그래밍': 2,
[0] '프로': 2,
[0] '용어': 2,
[0] '간접': 2,
[0] ES: 2,
[0] Classes: 2,
[0] '다음': 2,
[0] '의미': 2,
[0] object: 2,
[0] '연결': 2,
[0] '방식': 2,
[0] UI: 2,
[0] '모듈': 2,
[0] '키워드': 2,
[0] '호출': 2,
[0] '사용': 2,
[0] '드를': 2
[0] }" (type object) at path "keywords" because of "TypeError"
...
[0] reason: TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
[0] at new Map (<anonymous>)
[0] at new MongooseMap (/Users/junguk/Documents/boostcamp/MonumentGallery/server/node_modules/mongoose/lib/types/map.js:24:5)
[0] at Map.cast (/Users/junguk/Documents/boostcamp/MonumentGallery/server/node_modules/mongoose/lib/schema/map.js:59:12)
[0] at SchemaType.applySetters (/Users/junguk/Documents/boostcamp/MonumentGallery/server/node_modules/mongoose/lib/schematype.js:1201:12)
at EmbeddedDocument.$set (/Users/junguk/Documents/boostcamp/MonumentGallery/server/node_modules/mongoose/lib/document.js:1410:22)
[0] at EmbeddedDocument.$set (/Users/junguk/Documents/boostcamp/MonumentGallery/server/node_modules/mongoose/lib/document.js:1135:16)
[0] at EmbeddedDocument.Document (/Users/junguk/Documents/boostcamp/MonumentGallery/server/node_modules/mongoose/lib/document.js:166:12)
[0] at EmbeddedDocument.Subdocument (/Users/junguk/Documents/boostcamp/MonumentGallery/server/node_modules/mongoose/lib/types/subdocument.js:31:12)
[0] at EmbeddedDocument.ArraySubdocument [as constructor] (/Users/junguk/Documents/boostcamp/MonumentGallery/server/node_modules/mongoose/lib/types/ArraySubdocument.js:36:15)
[0] at new EmbeddedDocument (/Users/junguk/Documents/boostcamp/MonumentGallery/server/node_modules/mongoose/lib/schema/documentarray.js:134:17),
[0] valueType: 'object'
[0] }
[0] },
[0] _message: 'gallery validation failed'
[0] }
분명 데이터 스키마는 key가 문자열이고, 값이 숫자인 오브젝트 자료형이고, 스키마도 값이 숫자인 Map 자료형이기 때문에 별로 문제가 될 것은 없어 보였습니다. 하지만 타입에러가 발생해서 팀원들이서 해당 문제를 해결하기 위한 논의를 하게 되었습니다.
원인은 객체에 들어간 constructor라는 키 때문이었습니다. constructor라는 키를 제외하면 잘 동작하는 것을 확인할 수 있었습니다. 어떻게 이런 결과가 나올 수 있었을까요? 에러 스택에 있는 Mongoose의 코드를 뜯어 보았습니다.
//node_modules/mongoose/lib/types/map.js:24:5
class MongooseMap extends Map {
constructor(v, path, doc, schemaType) {
if (getConstructorName(v) === 'Object') {
v = Object.keys(v).reduce((arr, key) => arr.concat([[key, v[key]]]), []);
}
super(v);
this.$__parent = doc != null && doc.$__ != null ? doc : null;
this.$__path = path;
this.$__schemaType = schemaType == null ? new Mixed(path) : schemaType;
this.$__runDeferred();
}
몽구스가 스키마가 Map 자료형으로 정의된 데이터를 가져올 때, 새로운 MongooseMap을 생성합니다. MongooseMap은 바닐라 자바스크립트의 Map 자료형을 상속한 클래스입니다. 우선 우리가 형변환할 객체(v)의 생성자 이름을 확인합니다. 만약 생성자 이름이 Object라면, 객체를 키와 값의 쌍인 배열로 만듭니다. 그리고 super(v)
를 호출합니다. Map 생성자는 이터러블 객체를 받아 새로운 맵 객체를 생성합니다. 하지만 모종의 사유로 v는 이터러블 객체가 아니라 원본 객체가 그대로 넘어가지게 되었습니다. Map 생성자에 이터러블이 아닌 일반 객체를 넣으면 object is not iterable (cannot read property Symbol(Symbol.iterator))
에러를 반환합니다. 여기서 우리는 getConstructorName(v)
가 ‘Object’가 아닌 값이 나와서 그랬을 것이라고 추측할 수 있습니다.
//node_modules/mongoose/lib/helpers/getConstructorName.js:12:14
module.exports = function getConstructorName(val) {
if (val == null) {
return void 0;
}
if (typeof val.constructor !== 'function') {
return void 0;
}
return val.constructor.name;
};
val이 null이면 undefined를 반환합니다. 우리의 객체는 null이 아니므로 해당하지 않습니다.
val.constructor
가 함수인지를 판별합니다. 여기서 문제가 생겼네요. 우리의 객체에 constructor
라는 키를 가진 프로퍼티가 존재하고, 해당 프로퍼티가 함수가 아니기 때문에 getConstructorName
은 이 객체가 정말 객체임에도 불구하고 undefined라고 잘못 이해하게 되는 것이죠.