본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 27. · 21 Views
MongoDB와 Node.js 연동 완벽 가이드
Node.js 환경에서 MongoDB를 연결하고 활용하는 방법을 단계별로 알아봅니다. 기본 드라이버부터 Mongoose ODM까지, 실무에서 바로 사용할 수 있는 핵심 기술을 다룹니다.
목차
1. MongoDB Node.js Driver 설치
김개발 씨는 새 프로젝트에 투입되었습니다. 이번에는 MySQL 대신 MongoDB를 사용한다고 합니다.
"NoSQL이라... 어디서부터 시작해야 하지?" 막막한 마음으로 선배에게 질문했습니다.
"일단 드라이버부터 설치해야죠."
MongoDB Node.js Driver는 Node.js 애플리케이션에서 MongoDB 서버와 통신할 수 있게 해주는 공식 라이브러리입니다. 마치 한국어를 영어로 번역해주는 통역사처럼, 자바스크립트 코드를 MongoDB가 이해할 수 있는 명령어로 변환해줍니다.
이 드라이버를 설치하면 데이터베이스 연결부터 CRUD 작업까지 모든 것이 가능해집니다.
다음 코드를 살펴봅시다.
// 터미널에서 MongoDB 드라이버 설치
// npm install mongodb
const { MongoClient } = require('mongodb');
// MongoDB 연결 문자열 설정
const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri);
async function connectDB() {
try {
// 데이터베이스에 연결 시도
await client.connect();
console.log('MongoDB 연결 성공!');
// 데이터베이스 선택
const db = client.db('myapp');
return db;
} catch (error) {
console.error('연결 실패:', error);
}
}
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 지금까지는 MySQL만 사용해왔는데, 새 프로젝트에서 MongoDB를 도입한다는 소식을 들었습니다.
"NoSQL이 뭔지는 알겠는데, 실제로 어떻게 연결하는 거지?" 선배 개발자 박시니어 씨가 김개발 씨의 모니터를 보며 말했습니다. "MongoDB를 Node.js에서 사용하려면 먼저 공식 드라이버를 설치해야 해요.
생각보다 간단합니다." 그렇다면 MongoDB Node.js Driver란 정확히 무엇일까요? 쉽게 비유하자면, 드라이버는 마치 외국에 나갔을 때 현지인과 소통하게 해주는 통역사와 같습니다.
우리가 자바스크립트로 "이 데이터를 저장해줘"라고 말하면, 드라이버가 이를 MongoDB가 이해할 수 있는 언어로 번역해서 전달합니다. 그리고 MongoDB의 응답도 다시 자바스크립트가 이해할 수 있는 형태로 돌려줍니다.
드라이버가 없던 시절에는 어땠을까요? 개발자들은 직접 네트워크 프로토콜을 구현하고, 바이너리 데이터를 파싱하고, 연결 상태를 관리해야 했습니다.
상상만 해도 머리가 아픈 작업입니다. 더 큰 문제는 버전이 바뀔 때마다 모든 것을 다시 구현해야 했다는 점입니다.
바로 이런 문제를 해결하기 위해 MongoDB에서 공식 드라이버를 제공합니다. npm install mongodb 한 줄이면 모든 준비가 끝납니다.
위의 코드를 살펴보겠습니다. 먼저 MongoClient를 require로 불러옵니다.
이것이 MongoDB와 대화하는 핵심 객체입니다. 그 다음 연결 문자열(uri)을 설정하는데, 로컬 개발 환경에서는 보통 localhost:27017을 사용합니다.
client.connect() 메서드가 실제 연결을 수행합니다. 이 부분이 비동기(async/await)로 처리되는 이유는 네트워크 통신에 시간이 걸리기 때문입니다.
연결이 성공하면 client.db() 메서드로 사용할 데이터베이스를 선택합니다. 실제 현업에서는 환경변수로 연결 문자열을 관리합니다.
개발, 스테이징, 프로덕션 환경마다 다른 데이터베이스를 사용하기 때문입니다. 또한 MongoDB Atlas 같은 클라우드 서비스를 사용하면 연결 문자열에 사용자 인증 정보도 포함됩니다.
주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 연결을 맺고 닫지 않는 것입니다.
연결은 비용이 드는 자원이므로, 사용이 끝나면 반드시 **client.close()**를 호출해야 합니다. 물론 웹 서버처럼 계속 실행되는 애플리케이션에서는 연결을 유지하는 것이 효율적입니다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "생각보다 설치가 간단하네요!
이제 실제로 데이터를 다뤄볼 수 있겠어요."
실전 팁
💡 - 프로덕션 환경에서는 연결 문자열을 환경변수(process.env)로 관리하세요
- 연결 옵션에 useUnifiedTopology: true를 추가하면 최신 연결 관리 엔진을 사용합니다
2. 연결과 CRUD 구현
드라이버 설치를 마친 김개발 씨가 물었습니다. "연결은 됐는데, 이제 데이터는 어떻게 넣고 빼나요?" 박시니어 씨가 웃으며 답했습니다.
"CRUD라고 들어봤죠? Create, Read, Update, Delete.
이 네 가지만 알면 됩니다."
CRUD는 데이터베이스의 기본 작업 네 가지를 의미합니다. Create(생성), Read(조회), Update(수정), Delete(삭제)입니다.
마치 도서관에서 책을 등록하고, 찾아보고, 정보를 수정하고, 폐기하는 것과 같습니다. MongoDB에서는 insertOne, find, updateOne, deleteOne 메서드로 이 작업들을 수행합니다.
다음 코드를 살펴봅시다.
const { MongoClient } = require('mongodb');
const client = new MongoClient('mongodb://localhost:27017');
async function crudOperations() {
await client.connect();
const collection = client.db('myapp').collection('users');
// Create: 새 문서 삽입
const newUser = { name: '김개발', age: 28, role: 'developer' };
const insertResult = await collection.insertOne(newUser);
console.log('삽입된 ID:', insertResult.insertedId);
// Read: 문서 조회
const user = await collection.findOne({ name: '김개발' });
console.log('조회 결과:', user);
// Update: 문서 수정
await collection.updateOne(
{ name: '김개발' },
{ $set: { age: 29 } }
);
// Delete: 문서 삭제
await collection.deleteOne({ name: '김개발' });
await client.close();
}
김개발 씨는 드라이버 설치를 무사히 마쳤습니다. 이제 본격적으로 데이터를 다뤄볼 차례입니다.
"SQL에서는 INSERT, SELECT, UPDATE, DELETE를 썼는데, MongoDB에서는 어떻게 하나요?" 박시니어 씨가 화이트보드에 네 글자를 적었습니다. C.R.U.D. "이건 어떤 데이터베이스든 똑같아요.
방법만 조금 다를 뿐이죠." 그렇다면 MongoDB의 CRUD는 어떻게 동작할까요? 쉽게 비유하자면, MongoDB의 컬렉션은 마치 서류 캐비닛과 같습니다.
서류(문서)를 넣고, 찾고, 수정하고, 버리는 작업을 합니다. SQL의 테이블과 비슷하지만, 각 서류의 형식이 꼭 같을 필요가 없다는 점이 다릅니다.
먼저 Create 작업을 살펴봅시다. insertOne 메서드는 하나의 문서를 컬렉션에 추가합니다.
자바스크립트 객체를 그대로 전달하면 됩니다. MongoDB는 자동으로 _id 필드를 생성해서 고유 식별자를 부여합니다.
여러 문서를 한 번에 넣고 싶다면 insertMany를 사용합니다. Read 작업은 findOne과 find로 수행합니다.
findOne은 조건에 맞는 첫 번째 문서를 반환하고, find는 모든 문서를 커서 형태로 반환합니다. 조건은 객체 형태로 전달하는데, { name: '김개발' }처럼 작성하면 name 필드가 '김개발'인 문서를 찾습니다.
Update 작업이 조금 특이합니다. 첫 번째 인자로 찾을 조건을, 두 번째 인자로 수정할 내용을 전달합니다.
이때 $set 연산자를 사용하는데, 이것은 "이 필드만 바꿔라"라는 의미입니다. $set 없이 객체만 전달하면 문서 전체가 교체되어 버리니 주의해야 합니다.
Delete 작업은 가장 단순합니다. deleteOne에 삭제할 문서의 조건만 전달하면 됩니다.
deleteMany를 사용하면 조건에 맞는 모든 문서를 삭제합니다. 실무에서는 이 기본 작업들을 조합해서 복잡한 로직을 구현합니다.
예를 들어 회원가입은 insertOne, 로그인은 findOne, 프로필 수정은 updateOne, 탈퇴는 deleteOne으로 처리합니다. 주의할 점이 있습니다.
모든 작업이 비동기이므로 반드시 await를 붙여야 합니다. 또한 updateOne이나 deleteOne은 조건에 맞는 첫 번째 문서만 처리하므로, 여러 문서를 다루려면 Many 버전을 사용해야 합니다.
김개발 씨가 직접 코드를 실행해보았습니다. "오, 정말 간단하네요!
SQL보다 직관적인 것 같아요." 박시니어 씨가 답했습니다. "맞아요.
하지만 복잡한 쿼리는 연습이 필요해요."
실전 팁
💡 - updateOne에서 $set을 빼먹으면 문서 전체가 교체되니 항상 확인하세요
- find() 결과는 커서이므로 toArray()로 배열 변환하거나 forEach로 순회하세요
3. Mongoose ODM 소개
CRUD 실습을 마친 김개발 씨가 실제 프로젝트 코드를 열어보았습니다. "어?
저희 회사 코드는 좀 다르게 생겼는데요..." 화면에는 mongoose라는 낯선 이름이 보였습니다. 박시니어 씨가 설명을 시작했습니다.
"그건 ODM이라고 해요. 기본 드라이버보다 훨씬 편리하죠."
Mongoose는 MongoDB를 위한 ODM(Object Document Mapper) 라이브러리입니다. 마치 영어 원서를 한국어로 번역한 책처럼, MongoDB의 문서를 자바스크립트 객체로 더 편리하게 다룰 수 있게 해줍니다.
스키마 정의, 유효성 검사, 관계 설정 등 기본 드라이버에는 없는 강력한 기능들을 제공합니다.
다음 코드를 살펴봅시다.
// npm install mongoose
const mongoose = require('mongoose');
// Mongoose로 MongoDB 연결
async function connectWithMongoose() {
try {
await mongoose.connect('mongodb://localhost:27017/myapp');
console.log('Mongoose 연결 성공!');
// 연결 상태 확인
// 0: 연결 해제, 1: 연결됨, 2: 연결 중, 3: 연결 해제 중
console.log('연결 상태:', mongoose.connection.readyState);
// 연결 이벤트 리스너
mongoose.connection.on('error', (err) => {
console.error('연결 에러:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('연결이 끊어졌습니다');
});
} catch (error) {
console.error('연결 실패:', error);
}
}
김개발 씨는 회사 프로젝트의 데이터베이스 관련 코드를 살펴보고 있었습니다. 앞서 배운 기본 드라이버와는 사뭇 다른 모습입니다.
Schema, Model 같은 생소한 개념들이 눈에 들어왔습니다. "이건 왜 이렇게 복잡해 보이죠?" 김개발 씨의 질문에 박시니어 씨가 답했습니다.
"복잡해 보이지만, 사실은 더 안전하고 편리한 방법이에요. Mongoose라고 해요." 그렇다면 Mongoose란 정확히 무엇일까요?
쉽게 비유하자면, 기본 MongoDB 드라이버가 외국어 원서라면 Mongoose는 번역서에 해설까지 달린 책과 같습니다. 원서도 읽을 수 있지만, 번역서가 있으면 이해가 훨씬 쉽고 빠릅니다.
Mongoose는 MongoDB와 자바스크립트 사이의 간극을 메워주는 다리 역할을 합니다. 기본 드라이버만 사용했을 때의 문제점은 무엇일까요?
가장 큰 문제는 데이터의 일관성입니다. MongoDB는 스키마가 없어서 자유롭지만, 그만큼 실수하기도 쉽습니다.
누군가 age 필드에 숫자 대신 문자열을 넣어도 에러가 나지 않습니다. 프로젝트가 커지면 이런 작은 실수들이 쌓여 큰 버그가 됩니다.
Mongoose는 이런 문제를 스키마로 해결합니다. "이 컬렉션의 문서는 이런 형태여야 해"라고 미리 정의해두면, 규칙에 맞지 않는 데이터는 저장되지 않습니다.
타입스크립트가 자바스크립트에 타입 안전성을 더한 것처럼, Mongoose는 MongoDB에 구조적 안전성을 더합니다. 위의 코드를 살펴보겠습니다.
mongoose.connect() 한 줄로 연결이 완료됩니다. 기본 드라이버처럼 client 객체를 따로 관리할 필요가 없습니다.
Mongoose가 내부적으로 연결 풀을 관리해주기 때문입니다. connection.readyState로 현재 연결 상태를 확인할 수 있습니다.
0은 연결 해제, 1은 연결됨을 의미합니다. 또한 이벤트 리스너를 등록해서 연결 상태 변화를 감지할 수 있습니다.
실무에서 Mongoose를 선호하는 이유는 분명합니다. 스키마로 데이터 구조를 명확히 하고, 미들웨어로 저장 전후 작업을 자동화하고, 가상 필드로 계산된 값을 제공할 수 있습니다.
팀 프로젝트에서 특히 빛을 발합니다. 주의할 점도 있습니다.
Mongoose는 기본 드라이버 위에 추상화 계층을 얹은 것이므로, 약간의 성능 오버헤드가 있습니다. 초당 수만 건의 요청을 처리해야 하는 극한 상황에서는 기본 드라이버가 나을 수 있습니다.
하지만 대부분의 프로젝트에서는 Mongoose의 편의성이 더 큰 가치를 제공합니다. 김개발 씨가 고개를 끄덕였습니다.
"그러니까 안전장치가 있는 버전이군요!" 박시니어 씨가 답했습니다. "맞아요.
이제 스키마를 정의하는 방법을 알아볼까요?"
실전 팁
💡 - mongoose.connect()는 프로미스를 반환하므로 앱 시작 시 await로 연결을 기다리세요
- 개발 환경에서는 mongoose.set('debug', true)로 쿼리 로그를 확인할 수 있습니다
4. 스키마와 모델 정의
Mongoose의 개념을 이해한 김개발 씨가 물었습니다. "그럼 스키마는 어떻게 만드나요?" 박시니어 씨가 새 파일을 열며 답했습니다.
"스키마는 설계도예요. 어떤 데이터가 들어올 수 있는지 미리 정해두는 거죠.
이걸 바탕으로 모델을 만들면 실제로 데이터를 다룰 수 있어요."
**스키마(Schema)**는 문서의 구조를 정의하는 청사진입니다. 마치 건물을 짓기 전 설계도를 그리는 것처럼, 어떤 필드가 있고 각 필드의 타입은 무엇인지 명시합니다.
**모델(Model)**은 이 스키마를 바탕으로 실제 데이터베이스 작업을 수행하는 클래스입니다. 스키마가 설계도라면, 모델은 그 설계도로 만든 공장입니다.
다음 코드를 살펴봅시다.
const mongoose = require('mongoose');
// 스키마 정의: 문서의 구조를 설계합니다
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: { type: Number, min: 0, max: 150 },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
createdAt: { type: Date, default: Date.now },
profile: {
bio: String,
website: String
},
tags: [String] // 문자열 배열
});
// 모델 생성: 스키마를 컬렉션에 연결합니다
const User = mongoose.model('User', userSchema);
// 모델을 사용한 CRUD
async function createUser() {
const user = new User({ name: '김개발', email: 'kim@dev.com' });
await user.save(); // 데이터베이스에 저장
}
Mongoose를 설치하고 연결까지 마친 김개발 씨. 이제 실제로 데이터 구조를 설계할 차례입니다.
"SQL에서는 CREATE TABLE로 테이블을 만들었는데, 여기서는 어떻게 하나요?" 박시니어 씨가 설명을 시작했습니다. "Mongoose에서는 스키마로 구조를 정의해요.
SQL의 테이블 정의와 비슷하지만, 더 유연하고 강력하죠." 그렇다면 스키마와 모델의 관계는 무엇일까요? 쉽게 비유하자면, 스키마는 건축 설계도이고 모델은 건설 회사입니다.
설계도(스키마)에는 "이 건물은 방이 3개고, 화장실은 2개야"라고 적혀 있습니다. 건설 회사(모델)는 이 설계도를 받아서 실제로 건물을 짓고, 수리하고, 철거하는 작업을 수행합니다.
위의 코드를 자세히 살펴보겠습니다. mongoose.Schema() 생성자에 객체를 전달해서 스키마를 정의합니다.
각 필드마다 타입과 옵션을 지정할 수 있습니다. type은 필드의 데이터 타입입니다.
String, Number, Date, Boolean, ObjectId 등을 사용할 수 있습니다. required: true는 이 필드가 반드시 있어야 한다는 의미입니다.
없으면 저장 시 에러가 발생합니다. unique: true는 이 필드의 값이 컬렉션 내에서 유일해야 한다는 의미입니다.
이메일처럼 중복되면 안 되는 값에 사용합니다. min과 max는 숫자의 범위를 제한합니다.
나이가 음수이거나 150을 넘으면 저장되지 않습니다. enum은 허용되는 값의 목록을 정의합니다.
role 필드에는 'user' 또는 'admin'만 들어갈 수 있습니다. default는 값이 지정되지 않았을 때 사용할 기본값입니다.
중첩 객체도 가능합니다. profile 필드 안에 bio와 website가 들어있는 것을 볼 수 있습니다.
배열 타입은 대괄호로 감싸서 정의합니다. [String]은 문자열 배열을 의미합니다.
스키마가 완성되면 **mongoose.model()**로 모델을 생성합니다. 첫 번째 인자는 모델 이름이고, 두 번째 인자는 스키마입니다.
모델 이름은 단수형으로 지정하면 Mongoose가 자동으로 복수형 소문자로 컬렉션을 만듭니다. 'User' 모델은 'users' 컬렉션과 연결됩니다.
실무에서는 모델 파일을 별도로 분리해서 관리합니다. models 폴더에 User.js, Product.js처럼 모델별로 파일을 만들고 export합니다.
이렇게 하면 코드가 깔끔해지고 재사용성도 높아집니다. 주의할 점이 있습니다.
unique 옵션은 MongoDB 인덱스를 생성하는데, 기존 데이터에 중복이 있으면 인덱스 생성에 실패합니다. 또한 스키마 정의 후에는 구조를 변경하기 어려우므로 처음에 신중하게 설계해야 합니다.
김개발 씨가 감탄했습니다. "와, SQL의 NOT NULL, UNIQUE 같은 제약조건이 여기도 있네요!" 박시니어 씨가 답했습니다.
"맞아요. 그리고 더 강력한 유효성 검사도 할 수 있어요."
실전 팁
💡 - 모델 이름은 단수형 파스칼케이스(User)로, 컬렉션은 자동으로 복수형 소문자(users)가 됩니다
- timestamps: true 옵션을 추가하면 createdAt과 updatedAt이 자동 관리됩니다
5. Validation과 Middleware
스키마 정의를 마친 김개발 씨가 데이터를 저장하려고 했습니다. 그런데 이메일 형식이 잘못된 데이터도 저장되어 버렸습니다.
"이메일이 test@인데 왜 들어가죠?" 박시니어 씨가 웃으며 답했습니다. "타입만 지정하면 형식까지는 검사 안 해요.
커스텀 Validation을 추가해야 합니다."
Validation은 데이터가 저장되기 전에 규칙을 검사하는 기능입니다. 마치 경비원이 신분증을 확인하는 것처럼, 올바른 데이터만 통과시킵니다.
Middleware는 저장 전후에 자동으로 실행되는 함수입니다. 비밀번호를 해시하거나, 로그를 남기거나, 관련 데이터를 업데이트하는 작업을 자동화할 수 있습니다.
다음 코드를 살펴봅시다.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, '이메일은 필수입니다'],
// 커스텀 validator로 이메일 형식 검사
validate: {
validator: function(v) {
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(v);
},
message: props => `${props.value}는 올바른 이메일이 아닙니다`
}
},
password: { type: String, required: true }
});
// Pre 미들웨어: 저장 전에 실행
userSchema.pre('save', async function(next) {
// 비밀번호가 변경되었을 때만 해시 처리
if (this.isModified('password')) {
const bcrypt = require('bcrypt');
this.password = await bcrypt.hash(this.password, 10);
}
next(); // 다음 단계로 진행
});
// Post 미들웨어: 저장 후에 실행
userSchema.post('save', function(doc) {
console.log(`${doc.email} 사용자가 저장되었습니다`);
});
김개발 씨는 회원가입 기능을 개발하고 있었습니다. 이메일과 비밀번호를 받아서 저장하는 간단한 로직입니다.
그런데 테스트 중 이상한 점을 발견했습니다. "이메일에 @만 넣어도 저장이 되네요?" 박시니어 씨가 코드를 확인했습니다.
"type: String만 지정하면 문자열인지만 확인해요. 이메일 형식까지 검사하려면 커스텀 validator가 필요합니다." 그렇다면 Validation이란 무엇일까요?
쉽게 비유하자면, Validation은 건물 입구의 보안 검색대와 같습니다. 신분증이 있는지(required), 나이가 적절한지(min/max), 허용된 물품인지(enum)를 확인합니다.
기본 검사로 부족하면 커스텀 검사기를 설치할 수 있습니다. 이메일 형식 확인처럼 특별한 규칙이 필요할 때 사용합니다.
위 코드에서 validate 옵션을 주목하세요. validator 함수는 값을 받아서 true 또는 false를 반환합니다.
true면 통과, false면 저장이 거부됩니다. message는 검증 실패 시 보여줄 에러 메시지입니다.
이제 Middleware를 살펴보겠습니다. 미들웨어는 특정 작업 전후에 자동으로 실행되는 함수입니다.
마치 고속도로의 톨게이트처럼, 지나가는 모든 차량(데이터)에 대해 일괄적으로 처리합니다. pre 미들웨어는 작업 전에 실행됩니다.
위 코드에서는 save 전에 비밀번호를 해시 처리합니다. isModified() 메서드로 특정 필드가 변경되었는지 확인할 수 있습니다.
이렇게 하면 비밀번호가 변경된 경우에만 해시 처리가 수행됩니다. next() 호출은 중요합니다.
이걸 호출해야 다음 단계로 진행됩니다. 만약 검증 로직에서 문제가 발견되면 next(new Error('에러 메시지'))로 작업을 중단할 수 있습니다.
post 미들웨어는 작업 후에 실행됩니다. 저장된 문서 객체를 인자로 받습니다.
로그를 남기거나, 다른 서비스에 알림을 보내거나, 통계를 업데이트하는 등의 작업에 활용합니다. 실무에서 미들웨어는 정말 유용합니다.
사용자가 삭제되면 관련 게시글도 함께 삭제하기, 주문이 생성되면 재고 차감하기, 문서가 수정되면 수정 이력 남기기 등의 작업을 자동화할 수 있습니다. 주의할 점이 있습니다.
pre 미들웨어에서 화살표 함수를 사용하면 안 됩니다. 화살표 함수는 this가 바인딩되지 않기 때문입니다.
this로 현재 문서에 접근해야 하므로 일반 함수를 사용해야 합니다. 김개발 씨가 감탄했습니다.
"비밀번호 해시를 매번 직접 처리 안 해도 되는군요!" 박시니어 씨가 답했습니다. "맞아요.
한 번 설정해두면 알아서 처리되니까 실수할 일도 없죠."
실전 팁
💡 - pre 미들웨어에서는 반드시 next()를 호출하거나 에러를 던져야 합니다
- 화살표 함수 대신 일반 함수를 사용해야 this로 문서에 접근할 수 있습니다
6. 가상 필드와 메서드
프로젝트가 진행되면서 김개발 씨는 새로운 요구사항을 받았습니다. "사용자 이름을 firstName과 lastName으로 분리해서 저장하되, fullName으로도 조회할 수 있게 해주세요." 데이터베이스에 fullName을 또 저장하자니 중복인 것 같고, 매번 계산하자니 번거롭습니다.
방법이 없을까요?
**가상 필드(Virtual)**는 데이터베이스에 저장되지 않지만 조회 시 계산되어 제공되는 필드입니다. 마치 엑셀의 수식 셀처럼, 다른 값들을 조합해서 새로운 값을 만들어냅니다.
인스턴스 메서드는 개별 문서에서 호출할 수 있는 함수이고, 스태틱 메서드는 모델 전체에서 호출할 수 있는 함수입니다.
다음 코드를 살펴봅시다.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
firstName: String,
lastName: String,
birthYear: Number
});
// 가상 필드: DB에 저장되지 않지만 조회 가능
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
}).set(function(name) {
const [first, last] = name.split(' ');
this.firstName = first;
this.lastName = last;
});
userSchema.virtual('age').get(function() {
return new Date().getFullYear() - this.birthYear;
});
// 인스턴스 메서드: 개별 문서에서 사용
userSchema.methods.introduce = function() {
return `안녕하세요, ${this.fullName}입니다. ${this.age}살이에요.`;
};
// 스태틱 메서드: 모델에서 사용
userSchema.statics.findByName = function(name) {
return this.find({ firstName: new RegExp(name, 'i') });
};
const User = mongoose.model('User', userSchema);
// 사용 예시
// const user = await User.findOne();
// console.log(user.fullName); // '김 개발'
// console.log(user.introduce()); // '안녕하세요, 김 개발입니다. 28살이에요.'
// const users = await User.findByName('김');
김개발 씨는 사용자 정보를 다루는 코드를 작성하고 있었습니다. firstName과 lastName을 따로 저장하는데, 화면에 표시할 때는 "김개발"처럼 합쳐서 보여줘야 합니다.
매번 조합하는 코드를 작성하자니 너무 번거롭습니다. "fullName 필드를 따로 만들까요?" 김개발 씨의 질문에 박시니어 씨가 고개를 저었습니다.
"데이터 중복은 좋지 않아요. 대신 가상 필드를 사용하면 됩니다." 그렇다면 가상 필드란 무엇일까요?
쉽게 비유하자면, 가상 필드는 계산기와 같습니다. 통장에 1000원과 2000원이 있을 때, 합계 3000원을 따로 저장하지 않죠?
필요할 때 더해서 보여주면 됩니다. 가상 필드도 마찬가지입니다.
firstName과 lastName을 더해서 fullName을 계산해서 제공합니다. 위 코드의 virtual() 메서드를 살펴보겠습니다.
첫 번째 인자는 가상 필드의 이름입니다. get() 함수는 값을 조회할 때 실행되고, set() 함수는 값을 설정할 때 실행됩니다.
fullName = '김 개발'로 설정하면 set 함수가 실행되어 firstName과 lastName이 각각 저장됩니다. age 가상 필드는 birthYear를 바탕으로 현재 나이를 계산합니다.
이렇게 하면 매년 나이를 업데이트할 필요가 없습니다. 조회할 때마다 현재 연도를 기준으로 계산되니까요.
이제 메서드를 살펴보겠습니다. 인스턴스 메서드는 개별 문서 객체에서 호출합니다.
user.introduce()처럼 특정 사용자 정보를 활용하는 기능을 구현할 때 사용합니다. 마치 사람에게 "자기소개해줘"라고 요청하는 것과 같습니다.
스태틱 메서드는 모델에서 직접 호출합니다. User.findByName('김')처럼 특정 문서가 아닌 컬렉션 전체를 대상으로 하는 기능을 구현할 때 사용합니다.
마치 회사에 "김씨 성을 가진 직원 목록 줘"라고 요청하는 것과 같습니다. 실무에서 가상 필드는 정말 유용합니다.
URL 생성(id로 /users/123 만들기), 포맷팅(가격에 쉼표 추가), 계산(총액, 평균), 관계 데이터 조합 등 다양한 곳에서 활용됩니다. 주의할 점이 있습니다.
가상 필드는 기본적으로 JSON 변환 시 포함되지 않습니다. API 응답에 포함하려면 스키마 옵션에 **toJSON: { virtuals: true }**를 추가해야 합니다.
또한 가상 필드로는 쿼리 조건을 걸 수 없습니다. 실제 저장된 값이 아니기 때문입니다.
김개발 씨가 눈을 빛냈습니다. "오, 이렇게 하면 코드가 훨씬 깔끔해지겠네요!" 박시니어 씨가 답했습니다.
"맞아요. 잘 활용하면 모델 하나로 많은 것을 할 수 있어요."
실전 팁
💡 - toJSON: { virtuals: true } 옵션을 추가해야 JSON 응답에 가상 필드가 포함됩니다
- 가상 필드는 쿼리 조건에 사용할 수 없으니, 검색이 필요한 데이터는 실제로 저장하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
EFK 스택 로깅 완벽 가이드
마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.
Grafana 대시보드 완벽 가이드
실시간 모니터링의 핵심, Grafana 대시보드를 처음부터 끝까지 배워봅니다. Prometheus 연동부터 알람 설정까지, 초급 개발자도 쉽게 따라할 수 있는 실전 가이드입니다.
분산 추적 완벽 가이드
마이크로서비스 환경에서 요청의 전체 흐름을 추적하는 분산 추적 시스템의 핵심 개념을 배웁니다. Trace, Span, Trace ID 전파, 샘플링 전략까지 실무에 필요한 모든 것을 다룹니다.
Spring MongoDB 완벽 가이드
MongoDB와 NoSQL의 핵심 개념부터 Spring Data MongoDB를 활용한 실전 CRUD까지, 초급 개발자도 쉽게 따라할 수 있는 완벽한 가이드입니다. JPA와의 비교를 통해 언제 MongoDB를 사용해야 하는지 명확하게 이해할 수 있습니다.