본문으로 건너뛰기

Next.js 풀스택 앱 만들기

Next.js 풀스택 앱 만들기Next.js App Router에서 서버·클라이언트 양쪽으로 ConnectBase를 활용합니다

중급프레임워크 연동17초읽기 약 3분업데이트 2026-04-14

Next.js App Router에서 서버·클라이언트 양쪽으로 ConnectBase를 활용합니다

이 튜토리얼에서 배울 내용
Public/Secret Key 분리
Server Components에서 데이터 조회
Server Actions으로 데이터 변경
API Routes 웹훅 처리

Next.js 풀스택 앱 만들기

Next.js의 강점은 서버와 클라이언트를 하나의 프로젝트에서 모두 다룰 수 있다는 것입니다. ConnectBase와 함께 사용하면 서버에서는 Secret Key로 안전하게, 클라이언트에서는 Public Key로 빠르게 데이터에 접근할 수 있습니다.

이 가이드에서는 App Router 기반으로 Server Components, Server Actions, API Routes를 모두 활용하는 방법을 다룹니다.

📌 이 가이드는 Next.js App Router에 대한 기본 지식이 있다고 가정합니다.

1. 설치

bash
npm install connectbase-client

2. 환경 변수 설정

Next.js에서는 환경 변수 이름에 따라 서버/클라이언트 접근이 결정됩니다:

  • NEXT_PUBLIC_으로 시작하면 → 클라이언트에서도 접근 가능 (Public Key용)
  • 그 외 → 서버에서만 접근 가능 (Secret Key용)

.env.local:

NEXT_PUBLIC_CONNECT_BASE_PUBLIC_KEY=cb_pk_...
CONNECT_BASE_SECRET_KEY=cb_sk_...  # 서버 사이드용

3. SDK 인스턴스 분리하기

ConnectBase SDK 인스턴스를 클라이언트용서버용 두 개로 나눕니다. 서버용은 Secret Key를 사용하므로 더 많은 권한을 가집니다 (데이터 삭제, 관리자 기능 등).

lib/connectbase.ts:

typescript
import ConnectBase from 'connectbase-client'

// 클라이언트 사이드
export const cb = new ConnectBase({
  publicKey: process.env.NEXT_PUBLIC_CONNECT_BASE_PUBLIC_KEY!
})

// 서버 사이드 (API Routes, Server Components)
export const cbServer = new ConnectBase({
  secretKey: process.env.CONNECT_BASE_SECRET_KEY!
})

4. Server Components에서 데이터 조회하기

Server Components는 서버에서 실행되므로 Secret Key를 안전하게 사용할 수 있습니다. async/await로 직접 데이터를 가져와 HTML로 렌더링합니다 — API 라우트를 별도로 만들 필요가 없습니다.

app/users/page.tsx:

tsx
import { cbServer } from '@/lib/connectbase'

const USERS_TABLE_ID = process.env.USERS_TABLE_ID!

interface UserRow { id: string; data: { name: string; email?: string } }

async function getUsers(): Promise<UserRow[]> {
  const response = await cbServer.database.queryData(USERS_TABLE_ID, {
    orderBy: [{ field: 'createdAt', direction: 'desc' }],
    limit: 20,
  })
  return response.data as UserRow[]
}

export default async function UsersPage() {
  const users = await getUsers()

  return (
    <div>
      <h1>사용자 목록</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.data.name}</li>
        ))}
      </ul>
    </div>
  )
}

5. Server Actions으로 데이터 생성/수정하기

Server Actions는 클라이언트의 폼 제출을 서버에서 처리하는 기능입니다. 'use server' 지시어를 사용하면 함수가 서버에서 실행되므로, Secret Key를 안전하게 사용할 수 있습니다.

app/actions.ts:

typescript
'use server'

import { cbServer } from '@/lib/connectbase'
import { revalidatePath } from 'next/cache'

const USERS_TABLE_ID = process.env.USERS_TABLE_ID!

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string
  const email = formData.get('email') as string

  await cbServer.database.createData(USERS_TABLE_ID, {
    data: { name, email },
  })

  revalidatePath('/users')
}

export async function deleteUser(userId: string) {
  await cbServer.database.deleteData(USERS_TABLE_ID, userId)

  revalidatePath('/users')
}

6. API Routes — 외부 웹훅 처리

외부 서비스(결제 완료 알림, GitHub 이벤트 등)에서 보내는 웹훅을 처리하는 API 엔드포인트를 만듭니다. 서명 검증으로 요청의 진위를 확인하는 것이 중요합니다.

app/api/webhook/route.ts:

typescript
import { NextRequest, NextResponse } from 'next/server'
import { cbServer } from '@/lib/connectbase'
import crypto from 'crypto'

export async function POST(request: NextRequest) {
  const body = await request.json()
  const signature = request.headers.get('x-connectbase-signature')

  // 서명 검증
  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(JSON.stringify(body))
    .digest('hex')

  if (signature !== expectedSignature) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }

  // 이벤트 처리
  console.log('Webhook event:', body.type)

  return NextResponse.json({ success: true })
}

7. 클라이언트 컴포넌트에서 Server Actions 호출하기

'use client' 컴포넌트에서 Server Actions를 호출하는 방법입니다. 폼을 제출하면 Server Action이 서버에서 실행되어 데이터를 저장합니다.

components/CreateUserForm.tsx:

tsx
'use client'

import { useState } from 'react'
import { createUser } from '@/app/actions'

export function CreateUserForm() {
  const [pending, setPending] = useState(false)

  async function handleSubmit(formData: FormData) {
    setPending(true)
    await createUser(formData)
    setPending(false)
  }

  return (
    <form action={handleSubmit}>
      <input name="name" placeholder="이름" required />
      <input name="email" type="email" placeholder="이메일" required />
      <button disabled={pending}>
        {pending ? '생성 중...' : '사용자 생성'}
      </button>
    </form>
  )
}

이 튜토리얼이 도움이 됐나요?