본문으로 건너뛰기

첫 번째 앱 만들기

간단한 Todo 앱을 만들면서 Connect Base의 핵심 기능을 배워봅니다.

🚦 사전 준비 (Prerequisites)

코드 작성 전에 콘솔에서 두 가지를 발급받아야 합니다:

항목위치형식
Public Key콘솔 → 앱 → 설정 → Public Keyscb_pk_xxx
Todo 테이블 ID콘솔 → 앱 → 데이터베이스 → 새 테이블tbl_xxx

발급 절차가 처음이라면 먼저 빠른 시작 가이드를 확인하세요.

테이블 스키마 (콘솔에서 만들 컬럼)

컬럼명타입필수
titlestring
completedboolean✓ (기본값 false)

id, created_at, updated_at 컬럼은 Connect Base가 자동으로 추가합니다.


1. 프로젝트 설정

bash
# Vite + React + TypeScript 프로젝트 생성
npm create vite@latest my-todo-app -- --template react-ts
cd my-todo-app

# Connect Base SDK 설치
npm install connectbase-client

2. 환경 변수 설정

.env 파일을 생성합니다:

bash
VITE_CONNECT_BASE_PUBLIC_KEY=cb_pk_your_public_key_here
VITE_TODO_TABLE_ID=tbl_your_table_id_here

💡 Vite에서는 VITE_ 접두사가 붙어야 클라이언트에서 접근 가능합니다.

3. SDK 설정

src/lib/connectbase.ts 파일을 생성합니다:

typescript
import ConnectBase from 'connectbase-client'

export const cb = new ConnectBase({
  publicKey: import.meta.env.VITE_CONNECT_BASE_PUBLIC_KEY
})

export const TODO_TABLE_ID = import.meta.env.VITE_TODO_TABLE_ID

4. Todo 타입 정의

src/types/todo.ts:

typescript
export interface Todo {
  id: string
  title: string
  completed: boolean
  created_at: string
  updated_at: string
}

5. Todo 컴포넌트

src/App.tsx:

tsx
import { useState, useEffect } from 'react'
import { ApiError } from 'connectbase-client'
import { cb, TODO_TABLE_ID } from './lib/connectbase'
import type { Todo } from './types/todo'

function App() {
  const [todos, setTodos] = useState<Todo[]>([])
  const [newTitle, setNewTitle] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)

  // Todo 목록 조회
  useEffect(() => {
    loadTodos()
  }, [])

  const loadTodos = async () => {
    try {
      setLoading(true)
      const result = await cb.database.queryData(TODO_TABLE_ID, {
        orderBy: 'created_at',
        orderDirection: 'desc',
        limit: 100,
      })
      // result.data 는 DataItem[] — 각 row는 { id, data: {...}, created_at, ... } 구조
      const items = result.data.map((row) => ({
        id: row.id,
        title: row.data.title as string,
        completed: row.data.completed as boolean,
        created_at: row.created_at,
        updated_at: row.updated_at,
      }))
      setTodos(items)
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '목록 조회 실패')
    } finally {
      setLoading(false)
    }
  }

  // Todo 추가
  const addTodo = async () => {
    if (!newTitle.trim()) return
    try {
      await cb.database.createData(TODO_TABLE_ID, {
        data: { title: newTitle, completed: false },
      })
      setNewTitle('')
      await loadTodos()
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '추가 실패')
    }
  }

  // Todo 완료 토글
  const toggleTodo = async (todo: Todo) => {
    try {
      await cb.database.updateData(TODO_TABLE_ID, todo.id, {
        data: { completed: !todo.completed },
      })
      await loadTodos()
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '수정 실패')
    }
  }

  // Todo 삭제
  const deleteTodo = async (id: string) => {
    try {
      await cb.database.deleteData(TODO_TABLE_ID, id)
      await loadTodos()
    } catch (e) {
      setError(e instanceof ApiError ? e.message : '삭제 실패')
    }
  }

  return (
    <div style={{ maxWidth: 480, margin: '40px auto', padding: 16 }}>
      <h1>My Todos</h1>

      {error && (
        <div style={{ color: 'red', marginBottom: 12 }}>
          ⚠️ {error}
        </div>
      )}

      <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
        <input
          type="text"
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && addTodo()}
          placeholder="새 할 일..."
          style={{ flex: 1, padding: 8 }}
        />
        <button onClick={addTodo}>추가</button>
      </div>

      {loading && <p>불러오는 중...</p>}

      {!loading && todos.length === 0 && (
        <p>아직 할 일이 없습니다. 첫 번째 할 일을 추가해보세요!</p>
      )}

      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map((todo) => (
          <li
            key={todo.id}
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: 8,
              padding: 8,
              borderBottom: '1px solid #eee',
            }}
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none', flex: 1 }}>
              {todo.title}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>삭제</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default App

6. 실행

bash
npm run dev

브라우저에서 http://localhost:5173 을 열면 Todo 앱이 실행됩니다.

완성된 기능

  • ✅ Todo 추가 (createData)
  • ✅ Todo 목록 조회 (queryData + orderBy)
  • ✅ Todo 완료 토글 (updateData)
  • ✅ Todo 삭제 (deleteData)
  • ApiError 기반 에러 처리
  • ✅ 로딩 / 빈 상태 UI

다음 단계