recent에는 1960년 이후의 최신 책 두 권, Hyperion과 Neuromancer가 이 순서로 title과 year만 담겨 있습니다. findOne은 처음 일치한 것(또는 null)을 반환하고, countDocuments는 가격이 10 이상인 책을 2권으로 셉니다. $gt: 1960을 $gt: 1985로 바꾸면 Hyperion만 남습니다.
수정과 삭제
updateOne(filter, update)은 처음 일치한 문서를 바꿉니다. 수정에는 연산자를 씁니다. $set은 필드를 교체하고, $inc는 숫자에 더하고, $unset은 필드를 지우고, $push는 배열에 추가합니다. updateMany와 deleteMany는 일치하는 모든 문서에 적용됩니다.
수정은 문서 하나와 일치해 그것을 바꿨으므로 Dune은 이제 가격이 10, 재고가 2이고, 재고가 없는 책을 지우면 1권이 남습니다. 수정에는 연산자가 반드시 필요합니다. $set 없이 { price: 10 }을 넘기면 드라이버가 거부합니다(문서 전체를 교체하는 호출은 replaceOne입니다).
ObjectId와 인덱스
모든 문서에는 _id가 있습니다. 직접 지정하지 않으면 드라이버가 ObjectId를 만듭니다. 문자열로 들어온 id(예를 들어 URL에서)로 문서를 찾으려면 new ObjectId(id)로 감싸세요. 인덱스는 필드 검색을 빠르게 하고, 유니크 인덱스는 MongoDB가 중복을 거부하게 합니다.
GET /books는 두 권을 모두 반환하고, 각각 _id가 있습니다. id 하나를 복사해 GET /books/<id>에 넣으면 한 권을 가져오고, {"title": "Foundation", "year": 1951} 같은 JSON 본문으로 POST /books를 보낸 뒤 다시 목록을 볼 수도 있습니다.
실제 앱은 연결 문자열을 코드에 쓰지 않고 process.env.MONGODB_URI에서 읽습니다.
const Book = mongoose.model('Book', new mongoose.Schema({ title: String, year: Number }));
await Book.create({ title: 'Dune', year: 1965 });
흔한 실수
toArray()를 잊는 것.find()는 문서가 아니라 커서를 반환합니다. 그대로 로그를 찍으면 커서 객체가 보입니다. toArray()를 호출하거나 for await로 순회하세요.
요청마다 연결하는 것.new MongoClient는 각자 자기 풀을 엽니다. 시작할 때 클라이언트를 하나 만들어 어디서나 재사용하세요.
스크립트에서 클라이언트를 닫지 않는 것. 열린 클라이언트는 코드가 끝난 뒤에도 프로세스를 살려 둡니다. 스크립트 끝에서 await client.close()를 호출하세요. 서버는 살아 있는 동안 계속 열어 둡니다.
자주 묻는 질문
mongodb 드라이버와 Mongoose 중 무엇을 써야 하나요?
드라이버는 공식적인 얇은 계층으로, 일반 객체와 MongoDB 자체 쿼리 언어로 작업합니다. Mongoose는 그 위에 스키마, 검증, 모델을 더하므로 모든 문서를 검사하고 싶을 때 도움이 됩니다. 둘 다 같은 데이터베이스와 통신하고, Mongoose는 내부에서 드라이버를 씁니다.
데이터베이스나 컬렉션을 먼저 만들어야 하나요?
아니요. MongoDB는 처음 문서를 넣을 때 둘 다 만듭니다. client.db('shop')과 db.collection('books')는 핸들만 반환합니다.
연결 문자열은 어디에 둬야 하나요?
MONGODB_URI 같은 환경 변수에 두고 process.env.MONGODB_URI로 읽으세요. 호스팅된 데이터베이스의 연결 문자열에는 비밀번호가 들어 있으므로 저장소에 절대 커밋하면 안 됩니다.
id로 검색하면 find가 문서를 반환하지 않는 이유는?
_id는 문자열이 아니라 ObjectId로 저장되므로 { _id: '65f1...' }은 아무것과도 일치하지 않습니다. 먼저 변환하세요. { _id: new ObjectId(id) }입니다.