홈으로 돌아가기

CGO와 프레임워크 없이 Go로 구현한 Headless CMS

외부 의존성 없이 랜딩 페이지용 최소 Go 헤드리스 CMS. modernc/sqlite, net/http, Bitrix24용 채널 사용. 완벽한 보안과 제로 설정 실행.

Go로 헤드리스 CMS 만들기: 의존성 없이 20 MB
Advertisement 728x90

순수 Go로 구현한 미니멀 헤드리스 CMS: 아키텍처와 구현 방법

핵심 결정 사항은 mattn/go-sqlite3 대신 modernc.org/sqlite를 사용하는 것입니다. 이는 SQLite의 순수 Go 포트로, 호환되는 database/sql API를 제공하며 C 컴파일러가 필요하지 않습니다.

import (
    "database/sql"
    _ "modernc.org/sqlite"
)

func Open(path string) (*sql.DB, error) {
    db, err := sql.Open("sqlite", path)
    if err != nil {
        return nil, err
    }

    // 동시 접근을 위한 WAL 모드
    pragmas := []string{
        "PRAGMA journal_mode=WAL",
        "PRAGMA busy_timeout=5000",
        "PRAGMA foreign_keys=ON",
        "PRAGMA synchronous=NORMAL",
    }
    for _, p := range pragmas {
        if _, err := db.Exec(p); err != nil {
            return nil, fmt.Errorf("pragma %q: %w", p, err)
        }
    }
    return db, nil
}

WAL 모드는 쓰기 중에도 병렬 읽기를 가능하게 하여, 동시에 API와 관리자 패널이 작동할 때 필수적입니다.

net/http를 활용한 라우팅과 미들웨어

Gin/Echo/Chi 대신 http.ServeMux를 사용함으로써 의존성 감소와 디버깅 간편화를 달성합니다:

Google AdInline article slot
mux := http.NewServeMux()

// 공개 API
mux.Handle("/api/leads", rateLimiter(http.HandlerFunc(h.CreateLead)))
mux.Handle("/api/news", http.HandlerFunc(h.ListNews))

// 관리자 패널
mux.Handle("/admin/", authMiddleware(sessionStore, adminHandler))

미들웨어 체인: logger → rateLimiter → auth → csrfCheck → handler. 각각 독립된, 테스트 커버리지가 확보된 함수입니다.

Bitrix24와 비동기 통합

CRM에 리드 제출은 Go 채널과 워커 풀을 통해 비동기적으로 처리되며, Redis나 Celery 없이도 가능합니다:

type Worker struct {
    db      *sql.DB
    queue   chan Job
    webhook string
    wg      sync.WaitGroup
}

func NewWorker(db *sql.DB, webhook string, poolSize int) *Worker {
    w := &Worker{
        db:      db,
        queue:   make(chan Job, 100),
        webhook: webhook,
    }
    for i := 0; i < poolSize; i++ {
        w.wg.Add(1)
        go w.process()
    }
    return w
}

func (w *Worker) Enqueue(job Job) {
    select {
    case w.queue <- job:
    default:
        log.Printf("bitrix queue full, lead %d will be retried manually", job.LeadID)
    }
}

100건의 작업 버퍼, 30초 타임아웃을 포함한 격리된 종료 기능이 있습니다.

Google AdInline article slot

HMAC-SHA256 기반 CSRF 보호

HTMX를 사용하는 관리자 폼은 동기화된 토큰으로 보호됩니다:

func generateCSRFToken(sessionID, secret string) string {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(sessionID))
    return hex.EncodeToString(mac.Sum(nil))
}

func validateCSRFToken(r *http.Request, sessionID, secret string) bool {
    token := r.FormValue("csrf_token")
    if token == "" {
        token = r.Header.Get("X-CSRF-Token")
    }
    expected := generateCSRFToken(sessionID, secret)
    return hmac.Equal([]byte(token), []byte(expected))
}

토큰은 폼과 HTMX 헤더에 내장되어 있으며, hmac.Equal은 타이밍 공격을 방지합니다.

메모리 기반 요청 제한

공개 API는 외부 저장소 없이 IP당 분당 10회 요청만 허용합니다:

Google AdInline article slot
type IPLimiter struct {
    visitors sync.Map
    mu       sync.Mutex
}

type entry struct {
    count   int
    resetAt time.Time
}

func (l *IPLimiter) Allow(ip string) bool {
    now := time.Now()
    val, _ := l.visitors.LoadOrStore(ip, &entry{resetAt: now.Add(time.Minute)})
    e := val.(*entry)

    l.mu.Lock()
    defer l.mu.Unlock()

    if now.After(e.resetAt) {
        e.count = 0
        e.resetAt = now.Add(time.Minute)
    }
    if e.count >= 10 {
        return false
    }
    e.count++
    return true
}

5분마다 정리되는 sync.Map 사용.

설정 없이 첫 실행

서버는 데이터베이스를 자동 초기화하고 관리자 비밀번호를 생성해 콘솔에 출력합니다. 플래그나 환경 변수를 통해 구성 가능:

  • ./cms — 기본값 (포트=8080, db=./cms.db)
  • CMS_PORT=8080 CMS_DB_PATH=./data.db ./cms

보안 조치

| 위협 | 대응 방안 |

|--------|------------|

| SQL 인젝션 | 파라미터화된 쿼리 |

| XSS | 자동 에스케이프를 지원하는 html/template |

| CSRF | HMAC 토큰 |

| 브루트 포스 | 분당 10회 요청 제한 |

| 경로 탐색 | 샌드박스 내에서 http.FileServer 사용 |

주요 특징:

  • embed.FS를 통해 정적 자산 포함, 바이너리 크기 약 20MB, 런타임 의존성 0개
  • 잠금 없이 동시성 보장 가능한 WAL SQLite
  • Redis/Celery 대체용 Go 채널
  • HMAC CSRF + html/template로 강력한 보안 구현
  • CGO_ENABLED=0 — 윈도우에서 리눅스로 크로스 컴파일 가능

— Editorial Team

Advertisement 728x90

다음 읽기