언제까지고 DB를 멀리할 수는 없다. 암만 프론트라지만 DB를 한 번 찍어먹어 보기라도 해야하지 않겠는가. 몽고DB를 시작으로 DB와 조금씩 친해져보자.
MongoDB는 비정형 데이터를 처리하기에 적합한 NoSQL 데이터베이스로, 유연성과 확장성이 뛰어나다는 특징이 있다.
Next.js는 서버와 클라이언트를 통합한 React 프레임워크로, 서버에서 데이터를 처리하거나 클라이언트와 상호작용하는 작업을 손쉽게 구현할 수 있다.
Next.js의 API Routes를 사용하면 백엔드처럼 클라이언트의 요청을 처리해 줄 수 있는데, MongoDB의 유연한 DB를 조회하고 수정하는 API를 구축해보려고 한다.
데이터베이스(Database)는 일반적으로 컴퓨터 시스템에 저장된 정보 또는 데이터의 집합을 의미한다. 이는 체계적으로 정리되어 있어 데이터를 효율적으로 저장, 관리, 검색, 수정할 수 있도록 설계된 시스템이다.
데이터베이스는 서로 연관성이 있는 자료(Data)를 체계적으로 모아둔 기반(Base)으로, 대규모 데이터를 어떻게 잘 처리할 것인지가 핵심 과제이다. 애플리케이션 상에서 메모리에 존재하는 데이터는 영구적으로 보존되지 않기 때문에, 데이터를 장기간 저장하고 필요할 때 조회 및 수정할 수 있도록 하기 위해 데이터베이스를 사용한다.
데이터베이스는 보통 데이터베이스 관리 시스템(DBMS)에 의해 제어되며, 데이터와 DBMS는 관련 애플리케이션들과 함께 '데이터베이스 시스템'이라고 불린다. 흔히 이를 줄여 '데이터베이스'라고 통칭하기도 한다.
데이터베이스는 데이터 구조와 사용 목적에 따라 크게 두 가지로 구분된다.
관계형 데이터베이스는 데이터를 테이블로 저장하며, 각 테이블에는 고정된 스키마(열 이름 및 데이터 타입)가 있다. 테이블 간에는 키(Key)를 통해 관계를 맺는다.
고객 테이블 (Customers)
고객 ID (CustomerID) | 이름 (Name) | 이메일 (Email) | 전화번호 (Phone) |
---|---|---|---|
1 | 홍길동 | hong@example.com | 010-1234-5678 |
2 | 이순신 | lee@example.com | 010-8765-4321 |
주문 테이블 (Orders)
주문 ID (OrderID) | 고객 ID (CustomerID) | 상품 (Product) | 수량 (Quantity) |
---|---|---|---|
101 | 1 | 노트북 | 1 |
102 | 2 | 스마트폰 | 2 |
Customers.CustomerID
와 Orders.CustomerID
를 통해 두 테이블 간 관계를 설정한다.SELECT Customers.Name, Orders.Product, Orders.Quantity
FROM Customers
JOIN Orders ON Customers.CustomerID = Orders.CustomerID
WHERE Customers.Name = '홍길동';
비관계형 데이터베이스는 데이터 구조가 유연하며, 데이터를 문서(Document), 키-값(Key-Value), 그래프(Graph), 또는 컬럼(Column-family) 형태로 저장한다.
고객 및 주문 데이터
{
"CustomerID": 1,
"Name": "홍길동",
"Email": "hong@example.com",
"Phone": "010-1234-5678",
"Orders": [
{
"OrderID": 101,
"Product": "노트북",
"Quantity": 1
},
{
"OrderID": 103,
"Product": "태블릿",
"Quantity": 1
}
]
}
db.customers.find({ Name: "홍길동" }, { Name: 1, Orders: 1 });
MongoDB는 NoSQL 데이터베이스의 대표적인 문서(Document) 기반 데이터베이스이다. 데이터를 JSON과 유사한 BSON(Binary JSON) 형식으로 저장하며, 고정된 스키마가 없어 데이터 구조가 유연하다.
다음과 같은 특징을 띈다.
클러스터는 여러 개의 MongoDB 인스턴스가 모여서 하나의 분산 데이터베이스 시스템을 이루는 구조.
MongoDB는 데이터를 여러 서버에 분산시켜 저장할 수 있으며, 이 때 여러 서버(노드)가 모여 클러스터를 형성한다.
데이터를 논리적으로 그룹화한 단위. MongoDB에서는 하나의 데이터베이스 내에서 여러 Collection을 포함할 수 있다.
특정 애플리케이션이나 기능에 맞춰 데이터를 저장하고 관리하는 단위로, 하나의 애플리케이션은 여러 데이터베이스를 사용할 수 있다.
DB 내에서 문서(Document)를 저장하는 단위. MongoDB는 NoSQL 데이터베이스로, 관계형 데이터베이스(RDBMS)에서의 테이블과 유사한 역할을 한다. MongoDB에서는 스키마가 유연하기 때문에 컬렉션 내의 문서들이 반드시 동일한 형식을 가질 필요는 없다.
컬렉션은 데이터를 문서 형식으로 저장하며, 관련 있는 데이터를 그룹화하는 역할을 한다. 예를 들어, "사용자" 데이터를 저장하는 컬렉션, "주문" 데이터를 저장하는 컬렉션 등이 있다.
MongoDB에서 저장할 데이터의 구조를 가리키며, 데이터베이스에 저장되는 데이터의 형식과 타입을 지정하는 역할을 한다. Collection의 각 필드가 어떤 타입을 가지는지, 필수 여부 등을 지정하는 것이 스키마다.
const UserSchema = new mongoose.Schema({
name: { type: String, required: true }, // 이름은 반드시 있어야 하고, 문자열로 저장
email: { type: String, required: true, unique: true }, // 이메일도 반드시 있어야 하고, 고유해야 함
age: { type: Number, required: false }, // 나이는 선택 사항, 숫자 타입
});
스키마를 바탕으로 실제 데이터를 생성하고, 저장하고, 조회하는 데 사용되는 동적 객체다.
const User = mongoose.model('User', UserSchema);
'User'
는 모델 이름. MongoDB에서 컬렉션 이름은 모델 이름을 소문자로 만든 후 복수형으로 저장된다. User
모델은 users
컬렉션과 연결.UserSchema
는 위에서 정의한 스키마.실제 프론트 코드 상에서 해당 Model을 통해 DB와 상호작용을 하게 된다.
MongoDB에서 Atlas로 DB 호스팅을 지원해주어서 DB를 어디서 돌려야 하나 부담이 덜었다. 만세!
MongoDB Atlas | 멀티 클라우드 개발자 플랫폼
홈페이지로 가서 회원가입을 해주고, 세팅을 진행해주자.
위의 블로그 글을 참고해서 진행하였다.
무료 버전으로 클러스터를 생성하고, 환경 설정을 해주자.
DB에 접근할 때 사용할 계정과, 접속 가능한 IP를 설정해야 한다. 테스트를 위해 모든 IP에서 접근이 가능하게끔 해주었다.
MongoDB에서 제공하는 GUI 클라이언트인 MongoDB Compass를 사용해 클러스터에 연결할 수 있다.
MongoDB Compass를 설치한 후 new connection에서 <db_password> 부분에 아까 설정해준 비빌번호를 치환해주자.
연결 된 것을 확인할 수 있다!
이제야 본론이다. MongoDB를 Next.js 애플리케이션에 연동하여 데이터를 조회하고, SSR(Server-Side Rendering)을 통해 렌더링하는 과정을 진행해보자.
Mongoose는 MongoDB Atlas(클라우드) 또는 로컬 MongoDB와 쉽게 연결할 수 있게 도와주는 역할을 한다.
프로젝트 상에서 mongoose를 설치를 하고 MONGODB_URI을 환경 변수로 설정해준다.
npm install mongoose
// .env
MONGODB_URI="mongodb+srv://root:<db_password>@gulsamdb.pcem7.mongodb.net/?retryWrites=true&w=majority&appName=GulSamDB"
dbConnect
함수는 MongoDB와 애플리케이션을 연결하면서, 연결 정보를 전역 캐시에 저장하여 중복 연결을 방지하는 역할을 한다. 매번 DB에 접속할 때마다 연결하는 것도 방법이겠지만, 이렇게 중복 연결을 확인하고 방지하는 모듈을 활용한다면 불필요한 연결을 방지할 수 있다.
global.mongoose
를 활용해 중복 연결을 방지..env.local
파일에 MONGODB_URI
를 정의.sample_mflix
)를 지정.// src\mongoDB\dbConnect.ts
import mongoose from 'mongoose';
const url = process.env.MONGODB_URI;
if (!url) {
throw new Error('Please define the MONGODB_URI environment variable inside .env.local');
}
// @ts-ignore
let cached = global.mongoose;
if (!cached) {
// @ts-ignore
cached = global.mongoose = { conn: null, promise: null };
}
const dbConnect = async () => {
if (cached.conn) {
console.log('Already connected');
return cached.conn;
}
if (!cached.promise) {
cached.promise = mongoose
.connect(url, {
dbName: 'sample_mflix', // 여기서 'sample_mflix' 데이터베이스 명시
})
.then(mongoose => mongoose);
}
try {
cached.conn = await cached.promise;
} catch (e) {
cached.promise = null;
throw e;
}
return cached.conn;
};
export default dbConnect;
MongoDB는 스키마리스(schema-less) 데이터베이스이지만, 데이터의 구조를 정의하면 더 체계적으로 데이터를 다룰 수 있다.
Schema
: 데이터의 구조(필드명, 데이터 타입 등)를 정의.Model
: 정의된 스키마를 기반으로 데이터를 다루는 기능(읽기, 쓰기, 삭제 등)을 제공.mongoose.models.User || mongoose.model<IUser>('User', UserSchema)
를 통해 모델을 생성. 모델이 이미 존재하는 경우 재정의하지 않고 기존 모델을 사용.// src\mongoDB\User.ts
import mongoose, { Document, Schema } from 'mongoose';
interface IUser extends Document {
name: string;
email: string;
password: string;
}
const UserSchema: Schema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
});
export default mongoose.models.User || mongoose.model<IUser>('User', UserSchema);
Next.js의 API Routes는 서버에서 데이터를 처리하고 클라이언트와 통신하는 역할을 한다. 프론트엔드와 백엔드가 하나의 코드베이스에서 동작하기 때문에, API를 별도로 구축할 필요 없이 간단히 서버 로직을 작성할 수 있다는 점이 상당히 강력하다.
사용자 데이터를 MongoDB에서 조회해 클라이언트에 반환하는 코드로, GET 메서드 요청 시 dbConnect
를 호출한 후 User.find({})
를 통해 모든 사용자를 검색하고, 결과를 JSON 형태로 클라이언트에 반환한다.
// src\app\api\users\route.ts
import { NextResponse } from 'next/server';
import { dbConnect, User } from '@/mongoDB';
export async function GET() {
try {
await dbConnect();
const users = await User.find({});
return NextResponse.json(users, { status: 200 });
} catch (error) {
return NextResponse.json({ error: 'GET error', status: 500 });
}
}
이렇게 mongoose를 통해 직접 조작할 수도 있지만, 유지보수 및 설계를 생각하면 적절한 방법은 아닐 것이다.
import { NextResponse } from 'next/server';
import { dbConnect} from '@/mongoDB';
import mongoose from 'mongoose';
export async function GET() {
try {
await dbConnect();
const usersCollection = mongoose.connection.db.collection('users');
const users = await usersCollection.find({}).toArray();
return NextResponse.json(users, { status: 200 });
} catch (error) {
return NextResponse.json({ error: 'GET error', status: 500 });
}
}
axios.get('http://localhost:3000/api/users')
로 API에서 사용자 데이터를 가져온다.result.data
를 통해 받아온 데이터를 맵핑하여 이름과 이메일을 화면에 출력.// src\app\db\page.tsx
import axios from 'axios';
const Home = async () => {
const result = await axios.get('http://localhost:3000/api/users');
const data = result.data.slice(0, 10);
return (
<div>
MONGODB!!
{data.map(user => (
<div key={user._id}>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
))}
</div>
);
};
export default Home;
성공적으로 연결된 것을 확인할 수 있다.