fetch에서 mongoose로 전환
빠르고 더럽게 fetch로 만들었던 API를 mongoose로 리팩토링하는 과정을 다룹니다.
한가지 함정이 있습니다. 또 deno deploy에서 사용할 수 없습니다. 앞으로 개발할 때는 docker image부터 찾겠습니다. ㅂㄷㅂㄷ
mongoDB fetch 리팩토링
import { config } from 'https://deno.land/x/dotenv@v3.2.2/mod.ts';
import CardRecord from '../model/cards.ts';
const MONGO_URI = Deno.env.get('MONGO_URI') || config()['MONGO_URI'];
const CARD_API_KEY = Deno.env.get('CARD_API_KEY') || config()['CARD_API_KEY'];
type Collection = {
dataSource: string;
database: string;
collection: string;
};
/**
* @see https://www.mongodb.com/developer/languages/rust/getting-started-deno-mongodb/
* 모든 method가 POST로 고정되어 있습니다. 특정 메서드에 맞게 갱신은 없습니다.
*/
class MongoAPI {
private static instance: MongoAPI;
private baseURL: string;
private options: {
method: string;
headers: { 'Content-Type': string; 'api-key': string };
body: BodyInit;
};
private cardBody: Collection;
private userBody: Collection;
private constructor() {
this.baseURL = MONGO_URI;
this.options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'api-key': CARD_API_KEY,
},
body: '',
};
this.cardBody = {
dataSource: 'Cluster0',
database: 'cards_db',
collection: 'cards',
};
this.userBody = {
dataSource: 'Cluster0',
database: 'cards_db',
collection: 'user',
};
}
static getInstance(): MongoAPI {
if (!MongoAPI.instance) {
MongoAPI.instance = new MongoAPI();
}
return MongoAPI.instance;
}
async getCards(userId: string) {
try {
const result = await fetch(`${this.baseURL}/find`, {
...this.options,
body: JSON.stringify({ ...this.cardBody, filter: { userId } }),
});
return result.json();
} catch (error) {
return error;
}
}
async getAllCards() {
try {
const result = await fetch(`${this.baseURL}/find`, {
...this.options,
body: JSON.stringify({ ...this.cardBody }),
});
return result.json();
} catch (error) {
return error;
}
}
async postCards(document: CardRecord) {
try {
const result = await fetch(`${this.baseURL}/insertOne`, {
...this.options,
body: JSON.stringify({
...this.cardBody,
document,
}),
});
return result.json();
} catch (error) {
return error;
}
}
async patchCards(document: CardRecord) {
try {
const {
getQuestion: question,
getAnswer: answer,
getStackCount: stackCount,
getSubmitDate: submitDate,
getId: $oid,
} = document;
const result = await fetch(`${this.baseURL}/updateOne`, {
...this.options,
body: JSON.stringify({
...this.cardBody,
filter: { _id: { $oid } },
update: {
$set: {
question,
answer,
stackCount,
submitDate,
},
},
}),
});
return result.json();
} catch (error) {
return error;
}
}
async deleteCards($oid: string) {
try {
const result = await fetch(`${this.baseURL}/deleteOne`, {
...this.options,
body: JSON.stringify({
...this.cardBody,
filter: { _id: { $oid } },
}),
});
return result.json();
} catch (error) {
return error;
}
}
async postUser({
email,
passwordHash,
passwordSalt,
}: {
email: string;
passwordHash: string;
passwordSalt: string;
}) {
try {
const result = await fetch(`${this.baseURL}/insertOne`, {
...this.options,
body: JSON.stringify({
...this.userBody,
document: { email, passwordHash, passwordSalt },
}),
});
return result.json();
} catch (error) {
return error;
}
}
async getUser(email: string) {
try {
const result = await fetch(`${this.baseURL}/findOne`, {
...this.options,
body: JSON.stringify({
...this.userBody,
filter: { email },
}),
});
const data = await result.json();
return data.document;
} catch (error) {
return error;
}
}
}
export default MongoAPI;
위는 전체 코드입니다.
singleton이 단점입니다. 또 모두 POST 요청을 통해 주고 받아야 합니다.
atlas에 연결하는 문제도 같이 있었습니다.
mongoose 설치
import mongoose from 'npm:mongoose';
의도적으로 여기까지 작성했습니다. 그리고 캐싱처리해서 최신버전을 가져오도록 했습니다.
{
"npm": {
"specifiers": {
"mongoose": "mongoose@7.4.0",
"mongoose@7.4.0": "mongoose@7.4.0"
}
}
}
이렇게 최신버전을 확인했습니다.
import mongoose from 'npm:mongoose@^7.4.0';
이렇게 해서 최신버전을 일단 명시했습니다.
연결
원래 DB랑 통신하는 URL이 있고 다른 URL이 있습니다.
Mongo_URI=https://data.mongodb-api.com/app/{APP_ID}/endpoint/data/beta
data-api 엔드포인트를 통해서 지금까지 연결했습니다. 위와 같은 생김새를 갖습니다.
https://cloud.mongodb.com/v2/(본인클러스터uuid)#/dataAPI
위는 Data API 탭 링크입니다.
하지만 이제 연결할 때는 다른 mongoDB atlas에 API가 아닌 다른 방식으로 연결할 것입니다.
https://cloud.mongodb.com/v2/(본인프로젝트uuid)#/setup/access
위는 SECURITY > Quick Start 링크입니다. 여기서 본인이 만든 유저의 비밀번호를 얻도록 합시다.
https://cloud.mongodb.com/v2/(본인프로젝트uuid)#/clusters
위는 Database 탭입니다. Connect에서 Driver를 선택합니다.
mongodb+srv://username:<password>@(클러스터이름).(클러스터uuid).mongodb.net/(여기는 생략)
위와 비슷한 형식이 보일 것입니다. 위 문자열을 .env에 저장합니다. 그리고 password는 아까 복사한 본인 유저 비밀번호를 넣습니다.
MONGO_URL=mongodb+srv://username:<password>@(클러스터이름).(클러스터uuid).mongodb.net/(여기는 생략)
import mongoose from 'npm:mongoose@^7.4.0';
const MONGO_URL = Deno.env.get('MONGO_URL') || config()['MONGO_URL'];
await mongoose.connect(MONGO_URL);
console.log(mongoose.connection.readyState);
이렇게 하고 다음명령으로 확인해봅니다.
deno task dev
명령 결과에 1이 나오면 연결 성공입니다.
이렇게 하고 에러뜨면 이제 저도 모릅니다. 또 mongoDB는 업데이트가 잦은편입니다. 이 설명의 수명은 상당히 짧습니다.
참고
Getting Started with Deno & MongoDB
Where is the password for the cluster?
MongoDB mongoose는 query를 생성자의 메서드로 합니다.
How to connect Mongoose with Deno
인스턴스와 생성자는 메서드가 다릅니다. 뭐 당연합니다. 하지만 쿼리를 하면 생성된 인스턴스로 할 것이라는 생각을 했습니다.
import mongoose from 'npm:mongoose@^7.4.0';
const MONGO_URL = Deno.env.get('MONGO_URL') || config()['MONGO_URL'];
await mongoose.connect(MONGO_URL);
const card = new Card({
userId: '1234',
question: 'asdf',
answer: 'asdf',
submitDate: Date.now(),
stackCount: 9999,
});
const foo = await card.save(); // 인스턴스를 DB에 저장
const foo = await Card.findOne({ userId: '1234' }); // 생성자 메서드로 query
console.log(foo);
findOne 메서드 찾다가 생성자의 메서드는 의심을 안해봤네요. ㅂㄷㅂㄷ 이거 때문에 스트레스 많이 받았네요.
mongoDB 개념
mongoDB에는 개념이 있습니다. 프로젝트, 클러스터, 콜렉션 입니다. 또 dataSource, database, collection이 있습니다. 자료를 저장하는 단위입니다.
const cardBody = {
dataSource: 'Cluster0',
database: 'cards_db',
collection: 'cards',
};
const userBody = {
dataSource: 'Cluster0',
database: 'cards_db',
collection: 'user',
};
저에게는 다음 문제가 있습니다. dataSource, database 설정을 어떻게 할 수 있는지 알아내야 합니다.
현재 database는 test로 기본선택이 되어 있습니다.
dataSource, database, collection 설정하는 법 찾기
const foo = {
dataSource: 'Cluster0',
database: 'cards_db',
collection: 'cards',
};
dataSource는 .env로 연결한 url에서 설정되어 있습니다.
MONGO_URL=mongodb+srv://username:<password>@(클러스터이름).(클러스터uuid).mongodb.net/
database는 url path로 접근합니다.
MONGO_URL=mongodb+srv://username:<password>@(클러스터이름).(클러스터uuid).mongodb.net/cards_db
이렇게 되면 database를 cards_db로 설정한 것입니다.
import mongoose from 'npm:mongoose@^7.4.0';
const MONGO_URL = Deno.env.get('MONGO_URL') || config()['MONGO_URL'];
await mongoose.connect(MONGO_URL);
console.log(mongoose.connection.name);
이렇게 하면 현재 연결한 database이름을 알아낼 수 있습니다.
MongoDB mongoose를 활용한 CRUD
async function getCardNew(userId: string) {
try {
return await Card.find({ userId });
} catch (error) {
return error;
}
}
async function postCardNew(document: Card) {
const card = new Card(document);
try {
return await card.save();
} catch (error) {
return error;
}
}
async function patchCardNew({
question,
answer,
stackCount,
submitDate,
userId,
_id,
}: Card) {
try {
return await Card.findByIdAndUpdate(_id, {
question,
answer,
stackCount,
submitDate,
userId,
});
} catch (error) {
return error;
}
}
async function deleteCardNew(_id: string) {
try {
return await Card.findByIdAndDelete(_id);
} catch (error) {
return error;
}
}
함수를 정의하는 문법이 극단적으로 단촐해졌습니다.
// 생성
const create = await postCardNew({
userId: '1234',
question: '도커는 독하다',
answer: '도큐사우르스가 더 독하다',
stackCount: 9999,
submitDate: new Date(),
});
console.log('create', create, create._id);
// 읽기
const read = await getCardNew('1234');
console.log('read', read, read?._id);
// 갱신
const update = await patchCardNew({
_id: create._id,
question: '도커는 돚거',
answer: '돚거가 도커',
stackCount: 0,
submitDate: new Date(),
userId: '1234',
});
console.log('update', update);
// 갱신한거 읽기(빼도됨)
const read2 = await getCardNew('1234');
console.log('read2', read2);
// 삭제
const deleteCard = await deleteCardNew(create._id);
console.log('deleteCard', deleteCard);
넣은 문자열이 이상하지만 잘 작동합니다. 저장하고 터미널 console에서 동작하는 피드백을 잘 봤습니다.
create {
question: "도커는 독하다",
answer: "도큐사우르스가 더 독하다",
submitDate: 2023-07-20T04:47:36.515Z,
stackCount: 9999,
userId: "1234",
_id: new ObjectId("64b8bc688e8d1c14d26554b1"),
__v: 0
} new ObjectId("64b8bc688e8d1c14d26554b1")
read [
{
_id: new ObjectId("64b8bc688e8d1c14d26554b1"),
question: "도커는 독하다",
answer: "도큐사우르스가 더 독하다",
submitDate: 2023-07-20T04:47:36.515Z,
stackCount: 9999,
userId: "1234",
__v: 0
}
] undefined
read2 [
{
_id: new ObjectId("64b8bc688e8d1c14d26554b1"),
question: "도커는 돚거",
answer: "돚거가 도커",
submitDate: 2023-07-20T04:47:36.826Z,
stackCount: 0,
userId: "1234",
__v: 0
}
]
update {
_id: new ObjectId("64b8bc688e8d1c14d26554b1"),
question: "도커는 독하다",
answer: "도큐사우르스가 더 독하다",
submitDate: 2023-07-20T04:47:36.515Z,
stackCount: 9999,
userId: "1234",
__v: 0
}
deleteCard {
_id: new ObjectId("64b8bc688e8d1c14d26554b1"),
question: "도커는 돚거",
answer: "돚거가 도커",
submitDate: 2023-07-20T04:47:36.826Z,
stackCount: 0,
userId: "1234",
__v: 0
}
여기서 의문이 생겼습니다. __v이란 무엇인가?
versionKey라고 합니다.
const cardSchema = new Schema<Card>(
{
question: { type: String, required: true },
answer: { type: String, required: true },
submitDate: { type: Date, required: true },
stackCount: { type: Number, required: true },
userId: { type: String, required: true },
},
{ versionKey: false } // 여기
);
versionKey: false 설정하면 안보입니다. 기본적으로 기록해주는게 편리한 것 같습니다.
create {
question: "도커는 독하다",
answer: "도큐사우르스가 더 독하다",
submitDate: 2023-07-20T05:17:04.002Z,
stackCount: 9999,
userId: "1234",
_id: new ObjectId("64b8c3500484da100f651571")
} new ObjectId("64b8c3500484da100f651571")
read [
{
_id: new ObjectId("64b8c3500484da100f651571"),
question: "도커는 독하다",
answer: "도큐사우르스가 더 독하다",
submitDate: 2023-07-20T05:17:04.002Z,
stackCount: 9999,
userId: "1234"
}
] undefined
read2 [
{
_id: new ObjectId("64b8c3500484da100f651571"),
question: "도커는 돚거",
answer: "돚거가 도커",
submitDate: 2023-07-20T05:17:04.291Z,
stackCount: 0,
userId: "1234"
}
]
update {
_id: new ObjectId("64b8c3500484da100f651571"),
question: "도커는 독하다",
answer: "도큐사우르스가 더 독하다",
submitDate: 2023-07-20T05:17:04.002Z,
stackCount: 9999,
userId: "1234"
}
deleteCard {
_id: new ObjectId("64b8c3500484da100f651571"),
question: "도커는 돚거",
answer: "돚거가 도커",
submitDate: 2023-07-20T05:17:04.291Z,
stackCount: 0,
userId: "1234"
}
이제는 사라졌습니다.
useNewUrlParser와 useUnifiedTopology란?
MongoParseError: options useCreateIndex, useFindAndModify are not supported
굳이 신경쓸 이유가 없습니다.
mongoose 6.0 버전부터 모두 기본적으로 true로 설정됩니다.
connection은 언제 닫아야 하는가?
when to open and close mongoose connections 구글링을 해봤습니다.
옛날 문서이지만 connection pool을 재사용해서 안 닫는 것을 권장한다고 합니다.
https://www.mongodb.com/community/forums/t/where-to-close-db-connection/1368
https://stackoverflow.com/questions/52067945/when-to-close-a-mongodb-connection
작년 질문이지만 통신마다 열고 닫지말고 열어두라고 합니다.
mongoDB collection 이름 변경
이 작업을 진행할 때 프론트엔드는 알필요 없습니다.
mongoDB에서 리소스 이름을 cards라고 명명했습니다. 잘못된 관례라고 합니다. 리소스이름은 단수형으로 작성하는 것이 올바릅니다.
await mongoose
.connect(MONGO_URL)
.then(() => {
console.log('connected');
let db = mongoose.connection.db;
return db.collection('cards').rename('card');
})
.then(() => {
console.log('rename successful');
})
.catch((e) => {
console.log('rename failed:', e.message);
});
이렇게 적용할 수 있었습니다.
https://stackoverflow.com/questions/43391592/mongoose-rename-collection
위 질문을 참고 했습니다.
ObjectId의 타입은?
https://stackoverflow.com/questions/54743461/how-to-define-mongoose-id-in-typescript-interface
import { Types } from 'npm:mongoose@^7.4.0';
interface Animal {
_id: Types.ObjectId;
name: string;
}
이렇게 정의하고 사용할 수 있습니다. 문제는 d.ts 내에서 사용하는 방법입니다.
type Card = {
_id?: import('npm:mongoose@^7.4.0').Types.ObjectId;
question: string;
answer: string;
submitDate: Date;
stackCount: number;
userId: string;
};
type User = {
_id?: import('npm:mongoose@^7.4.0').Types.ObjectId;
email: string;
passwordHash: string;
passwordSalt: string;
};
정말 충격입니다. 이렇게 외부 라이브러리 타입을 import하는게 가능할 줄 몰랐습니다.
https://stackoverflow.com/questions/39040108/import-class-in-definition-file-d-ts
에러 로그 Module not found "npm@^7.4.0
이것은 미제사건입니다.
git commit을 하고 github를 확인해보니까 에러가 발생했습니다.
DB랑 통신할 때 mongoose를 활용해서 CRUD를 더 간소하게 수정했습니다. 로컬 개발환경에서는 문제 없이 사용할 수 있었습니다. 하지만 deno deploy 배포환경에서 npm 식별자를 인식할 수 없는 것으로 보입니다.
개발팀이 언젠가 반영할 계획이라고 말만했습니다.
Support npm: specifiers #314 - deno deploy feedback
docker를 학습해서 배포환경과 개발환경의 일관성을 확보하는 것이 필요해보입니다.
그리고 이 문제는 배포환경을 바꾸기 전까지 해결할 수 없는 것으로 간주합니다.
배포환경을 변경하는 것으로 문제를 해결할 때는 아래 블로그를 활용해야 합니다.
시도 esm.sh
하지만 esm을 활용할 수 있는 방법을 발견했습니다.
import express from 'https://esm.sh/express?target=denonext';
const app = express();
app.get('/', (req, res) => {
res.send('Hello from Deno Deploy!');
});
app.listen(8080);
Node.js built-ins on Deno Deploy
여기서 npm을 사용할 수 있게 도입 예정이라고 말만합니다. 그것도 5월에 말한 것입니다.
예시를 보고 esm.sh를 발견했습니다.
npm package를 CDN으로 받을 수 있게 해주는 서비스로 보입니다.
Introducing: ESM CDN for NPM + Deno
위에서 cloudflare가 지원한다고 하네요.
import { config } from '../deps.ts';
import mongoose from 'https://esm.sh/mongoose@7.4.0';
const MONGO_URL = Deno.env.get('MONGO_URL') || config()['MONGO_URL'];
await mongoose.connect(MONGO_URL);
이렇게 해서 시도하니까 다음 에러가 발생합니다.
error: Uncaught TypeError: mongoose.connect is not a function
await mongoose.connect(MONGO_URL);
이렇게 되면 문제를 풀 수 없습니다.