오늘도 공부
Node.js + TypeScript로 SurrealDB 게시판 CRUD 구현하기 본문
소개
이 튜토리얼에서는 Node.js와 TypeScript를 사용하여 SurrealDB에 연결하고, 간단한 게시판의 CRUD(Create, Read, Update, Delete) 기능을 구현하는 방법을 단계별로 살펴보겠습니다.
사용 기술 스택
- Node.js: v18 이상
- TypeScript: v5.0 이상
- SurrealDB: v1.0 이상
- surrealdb.js: 공식 JavaScript SDK
학습 목표
- SurrealDB를 Node.js 환경에서 연결하는 방법 이해
- TypeScript를 활용한 타입 안전성 확보
- 게시판 CRUD 기능의 실제 구현
- SurrealDB의 쿼리 언어(SurrealQL) 사용법 습득
환경 설정
1. SurrealDB 설치 및 실행
먼저 SurrealDB를 설치합니다.
macOS/Linux:
curl -sSf https://install.surrealdb.com | sh
Windows (PowerShell):
iwr https://windows.surrealdb.com -useb | iex
Docker 사용:
docker run --rm --pull always -p 8000:8000 surrealdb/surrealdb:latest start
SurrealDB 서버 실행:
surreal start --log trace --user root --pass root memory
이 명령어는 다음을 수행합니다:
- 메모리 모드로 데이터베이스 실행 (재시작 시 데이터 삭제)
- 8000번 포트에서 HTTP/WebSocket 서버 시작
- 관리자 계정: root / root
2. Node.js 프로젝트 초기화
새로운 디렉토리를 생성하고 프로젝트를 초기화합니다.
mkdir surrealdb-board
cd surrealdb-board
npm init -y
3. 필요한 패키지 설치
# 의존성 패키지
npm install surrealdb.js
# 개발 의존성 패키지
npm install -D typescript @types/node ts-node nodemon
# TypeScript 초기화
npx tsc --init
4. TypeScript 설정
tsconfig.json 파일을 다음과 같이 수정합니다:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
5. package.json 스크립트 설정
package.json에 다음 스크립트를 추가합니다:
{
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
프로젝트 구조
다음과 같은 구조로 프로젝트를 구성합니다:
surrealdb-board/
├── src/
│ ├── config/
│ │ └── database.ts # 데이터베이스 연결 설정
│ ├── models/
│ │ └── Post.ts # 게시글 모델 정의
│ ├── services/
│ │ └── PostService.ts # CRUD 비즈니스 로직
│ └── index.ts # 진입점
├── package.json
└── tsconfig.json
디렉토리를 생성합니다:
mkdir -p src/config src/models src/services
데이터베이스 연결 설정
src/config/database.ts
데이터베이스 연결을 관리하는 싱글톤 클래스를 생성합니다.
import { Surreal } from 'surrealdb.js';
class Database {
private static instance: Database;
private db: Surreal;
private connected: boolean = false;
private constructor() {
this.db = new Surreal();
}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
public async connect(): Promise<void> {
if (this.connected) {
console.log('이미 데이터베이스에 연결되어 있습니다.');
return;
}
try {
// SurrealDB 서버에 연결
await this.db.connect('http://127.0.0.1:8000/rpc');
// 네임스페이스와 데이터베이스 선택
await this.db.use({
namespace: 'board_app',
database: 'board_db'
});
// 인증
await this.db.signin({
username: 'root',
password: 'root',
});
this.connected = true;
console.log('✓ SurrealDB 연결 성공');
} catch (error) {
console.error('✗ SurrealDB 연결 실패:', error);
throw error;
}
}
public async disconnect(): Promise<void> {
if (!this.connected) {
return;
}
try {
await this.db.close();
this.connected = false;
console.log('✓ SurrealDB 연결 종료');
} catch (error) {
console.error('✗ 연결 종료 중 오류:', error);
throw error;
}
}
public getDB(): Surreal {
if (!this.connected) {
throw new Error('데이터베이스에 연결되지 않았습니다.');
}
return this.db;
}
public isConnected(): boolean {
return this.connected;
}
}
export default Database;
주요 포인트:
- 싱글톤 패턴: 애플리케이션 전체에서 하나의 데이터베이스 연결만 사용
- 연결 상태 관리: connected 플래그로 중복 연결 방지
- 에러 처리: 연결 실패 시 명확한 에러 메시지 제공
- 네임스페이스와 데이터베이스: 논리적 분리를 위한 구조
데이터 모델 정의
src/models/Post.ts
게시글 데이터 구조를 TypeScript 인터페이스로 정의합니다.
export interface Post {
id?: string; // SurrealDB 자동 생성 ID (선택적)
title: string; // 제목
content: string; // 내용
author: string; // 작성자
createdAt?: Date; // 생성 시간
updatedAt?: Date; // 수정 시간
views?: number; // 조회수
}
export interface CreatePostDTO {
title: string;
content: string;
author: string;
}
export interface UpdatePostDTO {
title?: string;
content?: string;
}
타입 설명:
- Post: 전체 게시글 데이터 구조
- CreatePostDTO: 게시글 생성 시 필요한 데이터
- UpdatePostDTO: 게시글 수정 시 선택적으로 변경 가능한 필드
CRUD 기능 구현
src/services/PostService.ts
게시판의 핵심 비즈니스 로직을 구현합니다.
import Database from '../config/database';
import { Post, CreatePostDTO, UpdatePostDTO } from '../models/Post';
class PostService {
private db = Database.getInstance().getDB();
/**
* 게시글 생성
*/
async createPost(postData: CreatePostDTO): Promise<Post> {
try {
const post = {
...postData,
createdAt: new Date(),
updatedAt: new Date(),
views: 0,
};
const result = await this.db.create<Post>('posts', post);
console.log('✓ 게시글 생성 성공:', result);
return result;
} catch (error) {
console.error('✗ 게시글 생성 실패:', error);
throw error;
}
}
/**
* 모든 게시글 조회
*/
async getAllPosts(): Promise<Post[]> {
try {
const posts = await this.db.select<Post>('posts');
console.log(`✓ ${posts.length}개의 게시글 조회`);
return posts;
} catch (error) {
console.error('✗ 게시글 조회 실패:', error);
throw error;
}
}
/**
* 특정 게시글 조회 (ID로)
*/
async getPostById(id: string): Promise<Post | null> {
try {
const post = await this.db.select<Post>(`posts:${id}`);
if (!post || post.length === 0) {
console.log('✗ 게시글을 찾을 수 없습니다.');
return null;
}
// 조회수 증가
await this.incrementViews(id);
console.log('✓ 게시글 조회 성공:', post[0]);
return post[0];
} catch (error) {
console.error('✗ 게시글 조회 실패:', error);
throw error;
}
}
/**
* 게시글 수정
*/
async updatePost(id: string, updateData: UpdatePostDTO): Promise<Post | null> {
try {
// 게시글 존재 여부 확인
const existingPost = await this.getPostById(id);
if (!existingPost) {
return null;
}
const updatedData = {
...updateData,
updatedAt: new Date(),
};
const result = await this.db.merge<Post>(`posts:${id}`, updatedData);
console.log('✓ 게시글 수정 성공:', result);
return result;
} catch (error) {
console.error('✗ 게시글 수정 실패:', error);
throw error;
}
}
/**
* 게시글 삭제
*/
async deletePost(id: string): Promise<boolean> {
try {
await this.db.delete(`posts:${id}`);
console.log('✓ 게시글 삭제 성공');
return true;
} catch (error) {
console.error('✗ 게시글 삭제 실패:', error);
throw error;
}
}
/**
* 조회수 증가
*/
private async incrementViews(id: string): Promise<void> {
try {
await this.db.query(
`UPDATE posts:${id} SET views = views + 1`
);
} catch (error) {
console.error('✗ 조회수 증가 실패:', error);
}
}
/**
* 제목으로 게시글 검색
*/
async searchPostsByTitle(keyword: string): Promise<Post[]> {
try {
const result = await this.db.query<[Post[]]>(
`SELECT * FROM posts WHERE title CONTAINS $keyword ORDER BY createdAt DESC`,
{ keyword }
);
const posts = result[0] || [];
console.log(`✓ "${keyword}" 검색 결과: ${posts.length}개`);
return posts;
} catch (error) {
console.error('✗ 게시글 검색 실패:', error);
throw error;
}
}
/**
* 작성자별 게시글 조회
*/
async getPostsByAuthor(author: string): Promise<Post[]> {
try {
const result = await this.db.query<[Post[]]>(
`SELECT * FROM posts WHERE author = $author ORDER BY createdAt DESC`,
{ author }
);
const posts = result[0] || [];
console.log(`✓ ${author}의 게시글 ${posts.length}개 조회`);
return posts;
} catch (error) {
console.error('✗ 작성자별 게시글 조회 실패:', error);
throw error;
}
}
/**
* 최신 게시글 조회 (페이지네이션)
*/
async getRecentPosts(limit: number = 10, offset: number = 0): Promise<Post[]> {
try {
const result = await this.db.query<[Post[]]>(
`SELECT * FROM posts ORDER BY createdAt DESC LIMIT $limit START $offset`,
{ limit, offset }
);
const posts = result[0] || [];
console.log(`✓ 최신 게시글 ${posts.length}개 조회`);
return posts;
} catch (error) {
console.error('✗ 최신 게시글 조회 실패:', error);
throw error;
}
}
}
export default PostService;
구현된 기능:
- createPost: 새 게시글 생성
- 자동으로 생성 시간, 수정 시간, 조회수 초기화
- SurrealDB의 create() 메서드 사용
- getAllPosts: 전체 게시글 목록 조회
- select() 메서드로 테이블의 모든 레코드 조회
- getPostById: ID로 특정 게시글 조회
- 조회 시 자동으로 조회수 증가
- 레코드 ID 형식: posts:ID
- updatePost: 게시글 수정
- merge() 메서드로 부분 업데이트
- 수정 시간 자동 갱신
- deletePost: 게시글 삭제
- delete() 메서드로 레코드 삭제
- incrementViews: 조회수 증가
- SurrealQL을 사용한 원자적 업데이트
- searchPostsByTitle: 제목 검색
- CONTAINS 연산자로 부분 일치 검색
- getPostsByAuthor: 작성자별 조회
- 조건부 쿼리 실행
- getRecentPosts: 페이지네이션
- LIMIT와 START로 페이지 단위 조회
전체 코드
src/index.ts
모든 기능을 테스트하는 메인 파일입니다.
import Database from './config/database';
import PostService from './services/PostService';
async function main() {
const db = Database.getInstance();
const postService = new PostService();
try {
// 데이터베이스 연결
await db.connect();
console.log('\n========== 게시판 CRUD 테스트 ==========\n');
// 1. 게시글 생성
console.log('1. 게시글 생성');
const post1 = await postService.createPost({
title: 'SurrealDB 시작하기',
content: 'SurrealDB는 차세대 멀티모델 데이터베이스입니다.',
author: '김개발',
});
const post2 = await postService.createPost({
title: 'Node.js와 TypeScript',
content: 'TypeScript로 타입 안전한 Node.js 애플리케이션을 만들어봅시다.',
author: '이코딩',
});
const post3 = await postService.createPost({
title: 'SurrealDB의 장점',
content: '멀티모델, 실시간 쿼리, 그래프 기능 등 다양한 장점이 있습니다.',
author: '김개발',
});
console.log('\n');
// 2. 모든 게시글 조회
console.log('2. 모든 게시글 조회');
const allPosts = await postService.getAllPosts();
allPosts.forEach((post) => {
console.log(` - [${post.id}] ${post.title} (작성자: ${post.author})`);
});
console.log('\n');
// 3. 특정 게시글 조회
console.log('3. 특정 게시글 조회');
if (post1.id) {
const postId = post1.id.split(':')[1]; // 'posts:ID'에서 ID 추출
const foundPost = await postService.getPostById(postId);
if (foundPost) {
console.log(` 제목: ${foundPost.title}`);
console.log(` 내용: ${foundPost.content}`);
console.log(` 조회수: ${foundPost.views}`);
}
}
console.log('\n');
// 4. 게시글 수정
console.log('4. 게시글 수정');
if (post2.id) {
const postId = post2.id.split(':')[1];
await postService.updatePost(postId, {
title: 'Node.js와 TypeScript (수정됨)',
content: 'TypeScript로 타입 안전한 애플리케이션을 개발하는 방법을 알아봅니다.',
});
}
console.log('\n');
// 5. 제목으로 검색
console.log('5. 제목 검색: "SurrealDB"');
const searchResults = await postService.searchPostsByTitle('SurrealDB');
searchResults.forEach((post) => {
console.log(` - ${post.title}`);
});
console.log('\n');
// 6. 작성자별 게시글 조회
console.log('6. 작성자별 조회: "김개발"');
const authorPosts = await postService.getPostsByAuthor('김개발');
authorPosts.forEach((post) => {
console.log(` - ${post.title}`);
});
console.log('\n');
// 7. 최신 게시글 조회 (페이지네이션)
console.log('7. 최신 게시글 2개 조회');
const recentPosts = await postService.getRecentPosts(2, 0);
recentPosts.forEach((post) => {
console.log(` - ${post.title} (${post.createdAt})`);
});
console.log('\n');
// 8. 게시글 삭제
console.log('8. 게시글 삭제');
if (post3.id) {
const postId = post3.id.split(':')[1];
await postService.deletePost(postId);
}
console.log('\n');
// 9. 삭제 후 전체 게시글 조회
console.log('9. 삭제 후 전체 게시글 조회');
const finalPosts = await postService.getAllPosts();
console.log(` 총 ${finalPosts.length}개의 게시글이 남아있습니다.`);
finalPosts.forEach((post) => {
console.log(` - [${post.id}] ${post.title}`);
});
console.log('\n========== 테스트 완료 ==========\n');
} catch (error) {
console.error('에러 발생:', error);
} finally {
// 데이터베이스 연결 종료
await db.disconnect();
}
}
// 프로그램 실행
main();
테스트 및 실행
1. SurrealDB 서버 실행
터미널을 열고 SurrealDB 서버를 시작합니다:
surreal start --log trace --user root --pass root memory
2. 애플리케이션 실행
새 터미널을 열고 프로젝트 디렉토리에서:
npm run dev
3. 예상 출력
✓ SurrealDB 연결 성공
========== 게시판 CRUD 테스트 ==========
1. 게시글 생성
✓ 게시글 생성 성공: { id: 'posts:abc123', title: 'SurrealDB 시작하기', ... }
✓ 게시글 생성 성공: { id: 'posts:def456', title: 'Node.js와 TypeScript', ... }
✓ 게시글 생성 성공: { id: 'posts:ghi789', title: 'SurrealDB의 장점', ... }
2. 모든 게시글 조회
✓ 3개의 게시글 조회
- [posts:abc123] SurrealDB 시작하기 (작성자: 김개발)
- [posts:def456] Node.js와 TypeScript (작성자: 이코딩)
- [posts:ghi789] SurrealDB의 장점 (작성자: 김개발)
3. 특정 게시글 조회
✓ 게시글 조회 성공: { id: 'posts:abc123', ... }
제목: SurrealDB 시작하기
내용: SurrealDB는 차세대 멀티모델 데이터베이스입니다.
조회수: 1
...
4. 빌드 및 프로덕션 실행
개발이 완료되면 TypeScript를 JavaScript로 컴파일합니다:
npm run build
npm start
마무리
학습한 내용
이 튜토리얼을 통해 다음을 학습했습니다:
- SurrealDB 설치 및 실행: 로컬 환경에서 데이터베이스 서버 구동
- Node.js + TypeScript 프로젝트 설정: 타입 안전성을 갖춘 개발 환경 구성
- 데이터베이스 연결 관리: 싱글톤 패턴을 사용한 효율적인 연결 관리
- CRUD 기능 구현: 생성, 조회, 수정, 삭제 기능의 완전한 구현
- 고급 쿼리: 검색, 필터링, 페이지네이션 등의 실용적 기능
SurrealDB의 장점
이 프로젝트를 통해 확인한 SurrealDB의 장점:
- 간편한 설정: 복잡한 설정 없이 빠른 시작 가능
- 타입 안전성: TypeScript와의 완벽한 호환
- 직관적인 API: SQL과 유사한 쿼리 언어로 낮은 학습 곡선
- 실시간 기능: 라이브 쿼리로 실시간 데이터 동기화 가능
- 멀티모델: 하나의 데이터베이스에서 다양한 데이터 구조 지원
다음 단계
이 기본 구현을 바탕으로 다음과 같은 기능을 추가해볼 수 있습니다:
- 댓글 시스템: 게시글과 댓글 간의 관계 구현
- 실시간 업데이트: live() 메서드를 사용한 실시간 알림
- 인증 시스템: 사용자 로그인 및 권한 관리
- 파일 업로드: 이미지 첨부 기능
- 태그 시스템: 게시글 분류 및 태그 검색
- Express.js 통합: REST API 서버 구축
- GraphQL 통합: GraphQL 스키마 설계
- 그래프 쿼리: 사용자 간 관계 분석
참고 자료
문제 해결
연결 오류 발생 시:
- SurrealDB 서버가 실행 중인지 확인
- 포트 8000이 사용 가능한지 확인
- 방화벽 설정 확인
타입 오류 발생 시:
- @types/node 패키지가 설치되어 있는지 확인
- tsconfig.json 설정이 올바른지 확인
쿼리 실패 시:
- SurrealQL 문법 확인
- 네임스페이스와 데이터베이스가 올바르게 선택되었는지 확인
이제 SurrealDB를 사용한 게시판 CRUD 애플리케이션을 완성했습니다! 이 기본 구조를 바탕으로 더 복잡하고 실용적인 애플리케이션을 개발해보세요.
'스터디 > Node Js' 카테고리의 다른 글
| JWT 설계시 비번 변경시 토큰 무효화 방법 (0) | 2025.05.26 |
|---|---|
| NodeJs 면접 인터뷰 모음 98개 (0) | 2025.04.08 |
| Node로 GeoIP 서비스 개발 (0) | 2025.03.17 |
| Node로 인앱 결제 구독 검증 방법 (0) | 2025.02.16 |
| NestJS에서 .nvmrc 파일을 사용하여 Node.js 버전을 관리 (1) | 2025.02.12 |
