Recent Posts
Recent Comments
반응형
«   2025/10   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

오늘도 공부

Node.js + TypeScript로 SurrealDB 게시판 CRUD 구현하기 본문

스터디/Node Js

Node.js + TypeScript로 SurrealDB 게시판 CRUD 구현하기

행복한 수지아빠 2025. 10. 21. 11:43
반응형

소개

이 튜토리얼에서는 Node.jsTypeScript를 사용하여 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;

구현된 기능:

  1. createPost: 새 게시글 생성
    • 자동으로 생성 시간, 수정 시간, 조회수 초기화
    • SurrealDB의 create() 메서드 사용
  2. getAllPosts: 전체 게시글 목록 조회
    • select() 메서드로 테이블의 모든 레코드 조회
  3. getPostById: ID로 특정 게시글 조회
    • 조회 시 자동으로 조회수 증가
    • 레코드 ID 형식: posts:ID
  4. updatePost: 게시글 수정
    • merge() 메서드로 부분 업데이트
    • 수정 시간 자동 갱신
  5. deletePost: 게시글 삭제
    • delete() 메서드로 레코드 삭제
  6. incrementViews: 조회수 증가
    • SurrealQL을 사용한 원자적 업데이트
  7. searchPostsByTitle: 제목 검색
    • CONTAINS 연산자로 부분 일치 검색
  8. getPostsByAuthor: 작성자별 조회
    • 조건부 쿼리 실행
  9. 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

마무리

학습한 내용

이 튜토리얼을 통해 다음을 학습했습니다:

  1. SurrealDB 설치 및 실행: 로컬 환경에서 데이터베이스 서버 구동
  2. Node.js + TypeScript 프로젝트 설정: 타입 안전성을 갖춘 개발 환경 구성
  3. 데이터베이스 연결 관리: 싱글톤 패턴을 사용한 효율적인 연결 관리
  4. CRUD 기능 구현: 생성, 조회, 수정, 삭제 기능의 완전한 구현
  5. 고급 쿼리: 검색, 필터링, 페이지네이션 등의 실용적 기능

SurrealDB의 장점

이 프로젝트를 통해 확인한 SurrealDB의 장점:

  • 간편한 설정: 복잡한 설정 없이 빠른 시작 가능
  • 타입 안전성: TypeScript와의 완벽한 호환
  • 직관적인 API: SQL과 유사한 쿼리 언어로 낮은 학습 곡선
  • 실시간 기능: 라이브 쿼리로 실시간 데이터 동기화 가능
  • 멀티모델: 하나의 데이터베이스에서 다양한 데이터 구조 지원

다음 단계

이 기본 구현을 바탕으로 다음과 같은 기능을 추가해볼 수 있습니다:

  1. 댓글 시스템: 게시글과 댓글 간의 관계 구현
  2. 실시간 업데이트: live() 메서드를 사용한 실시간 알림
  3. 인증 시스템: 사용자 로그인 및 권한 관리
  4. 파일 업로드: 이미지 첨부 기능
  5. 태그 시스템: 게시글 분류 및 태그 검색
  6. Express.js 통합: REST API 서버 구축
  7. GraphQL 통합: GraphQL 스키마 설계
  8. 그래프 쿼리: 사용자 간 관계 분석

참고 자료

문제 해결

연결 오류 발생 시:

  • SurrealDB 서버가 실행 중인지 확인
  • 포트 8000이 사용 가능한지 확인
  • 방화벽 설정 확인

타입 오류 발생 시:

  • @types/node 패키지가 설치되어 있는지 확인
  • tsconfig.json 설정이 올바른지 확인

쿼리 실패 시:

  • SurrealQL 문법 확인
  • 네임스페이스와 데이터베이스가 올바르게 선택되었는지 확인

이제 SurrealDB를 사용한 게시판 CRUD 애플리케이션을 완성했습니다! 이 기본 구조를 바탕으로 더 복잡하고 실용적인 애플리케이션을 개발해보세요.

반응형