안녕하세요! 카카오톡 스타일의 가족/친구용 프라이빗 크로스플랫폼 메신저 ‘딩동(DingDong)’ 만들기 프로젝트 5편입니다.
지난 [4편]에서는 유그린 NAS의 저사양 하드웨어에서도 깃털처럼 가볍게 돌아가는 **Go 백엔드 API 엔진(chi 라우터, Graceful Shutdown, 25MB 초경량 Dockerfile, Redis 연동)**을 세팅했습니다.
이번 5편에서는 메신저 백엔드의 심장인 **데이터베이스 스키마(테이블 구조)**를 완벽하게 설계하고, Go 진영에서 가장 우아하고 강력한 데이터베이스 툴인 **sqlc**를 연동하는 과정을 다룹니다.
1. 메신저 데이터베이스 설계 시 반드시 고려해야 할 4가지
단순한 게시판이나 쇼핑몰과 달리, 카카오톡 스타일의 실시간 메신저는 데이터베이스 설계 시 매우 까다로운 요구사항들이 있습니다:
flowchart TD
A["메신저 DB 핵심 설계 원칙"] --> B["1. 방별 단조 증가 순번(seq)"]
A --> C["2. 1:1 채팅방 중복 생성 방지"]
A --> D["3. 개인정보 보호 연락처 해시 매칭"]
A --> E["4. 다중 기기 세션 및 읽음 확인"]
linkStyle default stroke-width:3px,stroke:#333333- 방별 단조 증가 순번 (
seq):- 시간(Timestamp)으로만 메시지를 정렬하면 기기 간 시계 오차나 1밀리초 내 다중 메시지 발생 시 순서가 뒤틀립니다.
- 방마다 1, 2, 3… 순번(
seq)을 원자적(Atomic)으로 부여해야 클라이언트가 끊겼다 다시 연결되었을 때 **”5번 이후의 메시지만 주세요!”**라며 누락 없이 완벽한 증분 동기화를 할 수 있습니다.
- 1:1 채팅방 중복 생성 방지 (
direct_key):- A가 B에게 대화를 걸고, B도 A에게 대화를 걸 때 서로 다른 1:1 방이 중복으로 생기면 안 됩니다.
- 두 유저의 UUID를 오름차순으로 정렬해 유니크 키(
작은UUID:큰UUID)로 강제하여 DB 레벨에서 중복을 원천 차단합니다.
- 전화번호 SHA-256 해시 매칭 (
phone_hash):- 개인정보 보호를 위해 평문 전화번호는 DB에 남기지 않고, E.164 국제 표준 전화번호 규격의 SHA-256 해시값만 저장하여 주소록 동기화 시 친구를 안전하게 매칭합니다.
- 다중 기기(모바일/태블릿/PC) 세션 관리:
- 한 사용자가 스마트폰, 아이패드, 맥북에서 동시 접속할 수 있으므로 기기별(
devices) 세션과 개별 푸시 토큰(FCM/APNs)을 독립적으로 관리합니다.
- 한 사용자가 스마트폰, 아이패드, 맥북에서 동시 접속할 수 있으므로 기기별(
2. 왜 무거운 ORM 대신 sqlc를 선택했을까?
Node.js나 Python에서는 TypeORM, Prisma, SQLAlchemy 같은 무거운 ORM을 흔히 사용합니다. 하지만 Go 언어에서는 ORM보다 **sqlc**가 압도적인 찬사를 받고 있습니다.
| 비교 항목 | 전통적인 무거운 ORM (GORM 등) | Go 진영의 표준 sqlc |
|---|---|---|
| 동작 방식 | 런타임에 리플렉션(Reflection)으로 SQL 동적 생성 | 컴파일 타임에 순수 Go 코드를 미리 생성 |
| 메모리 및 CPU | 객체 매핑 오버헤드로 메모리 소모 큼 | 순수 드라이버 수준의 극한의 속도와 초경량 RAM |
| 타입 안정성 | 런타임에 컬럼명 오타 에러 발생 가능 | SQL 문법 검사 + Go 구조체 타입 100% 보장 |
| SQL 학습 비용 | 프레임워크 전용 DSL 문법을 새로 배워야 함 | 개발자가 원래 알던 표준 PostgreSQL SQL 그대로 작성 |
💡 sqlc의 마법: 개발자가
.sql파일에 순수한 PostgreSQL 쿼리를 작성하고 터미널에서sqlc generate를 치면, sqlc가 PostgreSQL 문법을 파싱하여 Go 구조체와 함수 코드를 1밀리초 만에 자동으로 만들어 줍니다!
3. 전체 데이터베이스 스키마 (DDL) 및 파일 생성
3.1 파일 경로 및 폴더 구조 안내
마이그레이션 파일은 server/ 디렉토리 아래에 migrations/ 폴더를 만들어 관리합니다.
DB 도구(golang-migrate, sqlc)가 인식할 수 있도록 파일 이름 뒤에 .up.sql(적용용)과 .down.sql(롤백용)을 붙여주는 것이 표준 규칙입니다.
server/ ├─ migrations/ │ ├─ 000001_init_schema.up.sql # 👈 [지금 만들 파일: 테이블 생성 DDL] │ └─ 000001_init_schema.down.sql # [롤백용 파일: 테이블 삭제 DROP] ├─ queries/ # 5번에서 만들 SQL 쿼리들 ├─ sqlc.yaml # 4번에서 만들 sqlc 설정 ├─ cmd/api/main.go ├─ Dockerfile ├─ go.mod └─ go.sum
3.2 테이블 생성 스키마 (server/migrations/000001_init_schema.up.sql)
VS Code에서 server/migrations/ 폴더를 생성하고, 000001_init_schema.up.sql 파일을 만들어 아래 DDL 코드를 그대로 붙여넣습니다:
-- UUID 생성 확장 기능 활성화
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- 1. 사용자 (Users)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
display_name VARCHAR(50) NOT NULL,
status_message VARCHAR(100),
avatar_file_id UUID,
phone_hash VARCHAR(64) UNIQUE, -- E.164 번호의 SHA-256 해시값
role VARCHAR(10) NOT NULL DEFAULT 'user', -- 'admin' | 'user'
status VARCHAR(10) NOT NULL DEFAULT 'active', -- 'active' | 'suspended' | 'deleted'
invited_by UUID REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_users_phone_hash ON users(phone_hash);
-- 2. 로그인 계정 수단 (Auth Identities)
CREATE TABLE auth_identities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(10) NOT NULL, -- 'password' | 'google' | 'apple'
provider_uid VARCHAR(255) NOT NULL, -- 로그인 아이디 또는 소셜 sub
password_hash TEXT, -- Argon2id 해시 (비밀번호 로그인 시)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (provider, provider_uid)
);
-- 3. 기기 세션 및 푸시 토큰 (Devices)
CREATE TABLE devices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
platform VARCHAR(10) NOT NULL, -- 'android' | 'ios' | 'macos' | 'windows'
name VARCHAR(100), -- 예: "아빠 Galaxy S24 Ultra"
refresh_token_hash TEXT NOT NULL,
refresh_expires_at TIMESTAMPTZ NOT NULL,
fcm_token TEXT,
voip_token TEXT, -- iOS PushKit 통화용
app_version VARCHAR(20),
last_active_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_devices_user_id ON devices(user_id);
-- 4. 초대 코드 (Invite Codes)
CREATE TABLE invite_codes (
code CHAR(6) PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
max_uses INT NOT NULL DEFAULT 10,
used_count INT NOT NULL DEFAULT 0,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 5. 친구 관계 (Friendships)
CREATE TABLE friendships (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
friend_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tag VARCHAR(10) NOT NULL DEFAULT 'friend', -- 'family' | 'friend'
nickname VARCHAR(50),
blocked BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, friend_id)
);
-- 6. 채팅방 (Rooms)
CREATE TABLE rooms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(10) NOT NULL, -- 'direct' | 'group'
direct_key VARCHAR(80) UNIQUE, -- 1:1 방 중복 방지 키 ("작은UUID:큰UUID")
name VARCHAR(50),
avatar_file_id UUID,
last_seq BIGINT NOT NULL DEFAULT 0, -- 방 메시지 최신 순번
created_by UUID REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 7. 채팅방 참여 멤버 (Room Members)
CREATE TABLE room_members (
room_id UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(10) NOT NULL DEFAULT 'member', -- 'owner' | 'member'
last_read_seq BIGINT NOT NULL DEFAULT 0, -- 마지막으로 읽은 메시지 순번
joined_seq BIGINT NOT NULL DEFAULT 0, -- 입장 시점 순번 (이전 대화 숨김용)
muted BOOLEAN NOT NULL DEFAULT false,
pinned BOOLEAN NOT NULL DEFAULT false,
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (room_id, user_id)
);
CREATE INDEX idx_room_members_user ON room_members(user_id);
-- 8. 메시지 (Messages)
CREATE TABLE messages (
id UUID PRIMARY KEY, -- 클라이언트가 생성한 중복 방지용 UUID v7
room_id UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
seq BIGINT NOT NULL, -- 방 내 1씩 증가하는 단조 순번
sender_id UUID REFERENCES users(id), -- 탈퇴 시 NULL
type VARCHAR(10) NOT NULL, -- 'text' | 'image' | 'file' | 'call_log' | 'system'
text TEXT CHECK (char_length(text) <= 10000),
attachments JSONB, -- [{fileId, name, size, mime, width, height, thumbFileId}]
call_log JSONB, -- {callId, durationSec, result}
deleted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (room_id, seq) -- 증분 동기화 쿼리 고속 인덱스 겸용
);
CREATE INDEX idx_messages_room_created ON messages(room_id, created_at DESC);
-- 9. 파일 저장소 메타데이터 (Files)
CREATE TABLE files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
uploader_id UUID NOT NULL REFERENCES users(id),
message_id UUID REFERENCES messages(id) ON DELETE SET NULL,
purpose VARCHAR(10) NOT NULL, -- 'message' | 'avatar'
storage_path TEXT NOT NULL, -- NAS 저장 상대경로 (예: 2026/10/uuid.jpg)
name VARCHAR(255) NOT NULL,
mime_type VARCHAR(100),
size BIGINT NOT NULL,
sha256 CHAR(64),
status VARCHAR(10) NOT NULL DEFAULT 'uploading', -- 'uploading' | 'ready'
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 10. 보이스톡 통화 기록 (Calls & Call Participants)
CREATE TABLE calls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
caller_id UUID NOT NULL REFERENCES users(id),
status VARCHAR(10) NOT NULL, -- 'ringing'|'accepted'|'declined'|'missed'|'ended'
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
accepted_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ
);
CREATE TABLE call_participants (
call_id UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id),
joined_at TIMESTAMPTZ,
left_at TIMESTAMPTZ,
PRIMARY KEY (call_id, user_id)
);
3.3 롤백용 스키마 (server/migrations/000001_init_schema.down.sql)
마이그레이션 도구는 실수를 되돌릴 때를 대비해 테이블을 지우는 .down.sql도 필요로 합니다.
생성 순서의 역순으로 외래키 제약조건에 걸리지 않게 깔끔하게 지우는 코드입니다:
DROP TABLE IF EXISTS call_participants CASCADE; DROP TABLE IF EXISTS calls CASCADE; DROP TABLE IF EXISTS files CASCADE; DROP TABLE IF EXISTS messages CASCADE; DROP TABLE IF EXISTS room_members CASCADE; DROP TABLE IF EXISTS rooms CASCADE; DROP TABLE IF EXISTS friendships CASCADE; DROP TABLE IF EXISTS invite_codes CASCADE; DROP TABLE IF EXISTS devices CASCADE; DROP TABLE IF EXISTS auth_identities CASCADE; DROP TABLE IF EXISTS users CASCADE;
4. sqlc 설치 및 설정 (sqlc.yaml)
4.1 sqlc CLI 설치 (Mac 기준)
맥북 터미널에서 Homebrew로 간편하게 설치할 수 있습니다:
brew install sqlc
4.2 sqlc.yaml 설정 파일 작성 (실전 꿀팁: Engine 필수, UUID & Pointer 매핑)
server/sqlc.yaml 경로에 설정 파일을 생성합니다.
💡 핵심 팁 3가지: sqlc v2 + pgx/v5 완벽 호환 설정
engine: "postgresql": sqlc v2에서는 엔진 명시가 필수입니다. 빠지면engine is required에러가 발생합니다.emit_pointers_for_null_types: true: sqlc + pgx/v5 조합에서 이 옵션이 없으면 Nullable 컬럼이나sqlc.narg파라미터가 다루기 번거로운pgtype.Text같은 구조체로 생성됩니다. 이 옵션을 켜면 친숙한 Go의 네이티브 포인터(*string,*int등)로 생성되어 코드가 비약적으로 깔끔해집니다!- Nullable UUID의
pointer: true객체 문법: 문자열에*github...처럼*를 붙이면 sqlc 파서가 잘못된 import 코드를 생성합니다. 포인터 타입은 반드시 아래처럼import,type,pointer: true객체 형태로 선언해야 올바른*uuid.UUID코드가 생성됩니다.
version: "2"
sql:
- engine: "postgresql"
schema: "migrations"
queries: "queries"
gen:
go:
package: "db"
out: "internal/database/db"
sql_package: "pgx/v5"
emit_json_tags: true
emit_prepared_queries: false
emit_interface: true
emit_empty_slices: true
emit_pointers_for_null_types: true
overrides:
- db_type: "uuid"
go_type: "[github.com/google/uuid.UUID](https://github.com/google/uuid.UUID)"
- db_type: "uuid"
nullable: true
go_type:
import: "[github.com/google/uuid](https://github.com/google/uuid)"
type: "UUID"
pointer: true
5. SQL 쿼리 작성 및 자동 코드 생성 실습
이제 server/queries/ 폴더에 우리가 필요한 실제 쿼리를 작성해 봅니다.
5.1 회원 관련 쿼리 (server/queries/users.sql) – sqlc.narg로 안전한 선택적 업데이트
💡 핵심 팁:
sqlc.narg를 활용한 Nullable 파라미터 처리display_name컬럼이NOT NULL일 때 단순히COALESCE($2, display_name)을 쓰면sqlc가$2를 일반string으로 만들어 버립니다. Go에서 빈 문자열("")은 SQL의NULL이 아니므로, 이름을 바꾸지 않으려고 빈 값을 넘겼다가 이름이 공백으로 지워지는 대형 버그가 생깁니다.sqlc.narg('이름')을 쓰면 Go에서 포인터(*string) 타입으로 자동 생성되어,nil을 넘겼을 때 안전하게 기존 값을 유지합니다!
-- name: CreateUser :one
INSERT INTO users (display_name, status_message, phone_hash, role)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetUserByID :one
SELECT * FROM users
WHERE id = $1 AND status != 'deleted';
-- name: GetUserByPhoneHash :one
SELECT * FROM users
WHERE phone_hash = $1 AND status = 'active';
-- name: UpdateUserProfile :one
UPDATE users
SET display_name = COALESCE(sqlc.narg('display_name'), display_name),
status_message = COALESCE(sqlc.narg('status_message'), status_message),
avatar_file_id = COALESCE(sqlc.narg('avatar_file_id'), avatar_file_id),
updated_at = now()
WHERE id = sqlc.arg('id')
RETURNING *;
5.2 채팅 메시지 전송 쿼리 (server/queries/messages.sql) – CTE를 통한 원자성 확보
💡 핵심 팁: CTE(WITH 구문)로 트랜잭션 없이 1방에 원자적(Atomic) 발송
메시지 발송 시 **”방 최신 순번 1 증가(IncrementRoomSeq)”**와 **”메시지 삽입(InsertMessage)”**을 별도 쿼리로 나누면 네트워크 왕복이 2번 발생하고, 중간에 서버가 꺼질 경우 순번만 올라가고 메시지는 유실되는 불일치가 생길 수 있습니다.
PostgreSQL의 CTE(WITH ...) 구문을 쓰면 **단 1회의 쿼리로 순번 증가와 메시지 저장을 완벽한 원자성(Atomic)**으로 처리할 수 있습니다!
-- name: SendMessageWithSeq :one
WITH next_seq AS (
UPDATE rooms
SET last_seq = last_seq + 1,
updated_at = now()
WHERE id = $1
RETURNING last_seq
)
INSERT INTO messages (
id,
room_id,
seq,
sender_id,
type,
text,
attachments,
call_log
)
SELECT
$2,
$1,
next_seq.last_seq,
$3,
$4,
$5,
$6,
$7
FROM next_seq
RETURNING *;
-- name: GetMessagesAfterSeq :many
SELECT * FROM messages
WHERE room_id = $1 AND seq > $2
ORDER BY seq ASC
LIMIT $3;
5.3 마법의 한 줄 실행: sqlc generate
터미널에서 server/ 디렉토리로 이동한 뒤 아래 명령어를 실행합니다:
cd server sqlc generate
명령어가 끝나면 server/internal/database/db/ 폴더에 다음과 같은 Go 파일들이 마법처럼 생성됩니다:
models.go: 모든 테이블 구조체 (UUID가uuid.UUID로 매핑됨!)users.sql.go: 포인터 파라미터를 받는UpdateUserProfileParams및 조회 함수들messages.sql.go: CTE로 묶인 고성능SendMessageWithSeq함수db.go&querier.go: DB 인터페이스 및 트랜잭션 래퍼
6. Go 서버에 고성능 DB 연결 풀 (pgxpool) 및 트랜잭션(WithTx) 헬퍼 구현
단일 쿼리는 위처럼 해결할 수 있지만, “방 생성 + 여러 멤버 추가” 같은 비즈니스 로직은 Go 코드 차원에서 트랜잭션이 꼭 필요합니다.
이를 위해 깔끔한 WithTx 트랜잭션 래퍼를 구성합니다.
6.1 트랜잭션 래퍼 (server/internal/database/tx.go)
package database
import (
"context"
"fmt"
"[github.com/jackc/pgx/v5](https://github.com/jackc/pgx/v5)"
"[github.com/jackc/pgx/v5/pgxpool](https://github.com/jackc/pgx/v5/pgxpool)"
"[github.com/happyforevergithub/dingdong-server/internal/database/db](https://github.com/happyforevergithub/dingdong-server/internal/database/db)"
)
// WithTx: 트랜잭션 내에서 sqlc 쿼리를 안전하게 실행하고 성공 시 Commit, 실패 시 Rollback합니다.
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(q *db.Queries) error) error {
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return fmt.Errorf("트랜잭션 시작 실패: %w", err)
}
q := db.New(tx)
if err := fn(q); err != nil {
if rbErr := tx.Rollback(ctx); rbErr != nil {
return fmt.Errorf("롤백 실패 (%v): %w", rbErr, err)
}
return err
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("커밋 실패: %w", err)
}
return nil
}
6.2 서버 진입점 연결 (server/cmd/api/main.go)
package main
import (
"context"
"log"
"os"
"time"
"[github.com/jackc/pgx/v5/pgxpool](https://github.com/jackc/pgx/v5/pgxpool)"
"[github.com/happyforevergithub/dingdong-server/internal/database/db](https://github.com/happyforevergithub/dingdong-server/internal/database/db)"
)
func main() {
ctx := context.Background()
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
dbURL = "postgres://dingdong:dingdong_secret_2026@localhost:5432/dingdong?sslmode=disable"
}
// 1. 커넥션 풀 설정 (저사양 NAS 맞춤 튜닝)
config, err := pgxpool.ParseConfig(dbURL)
if err != nil {
log.Fatalf("DB 설정 파싱 실패: %v", err)
}
config.MaxConns = 25 // 최대 연결 수 제한 (RAM 절약)
config.MinConns = 5 // 유휴 최소 연결 유지
config.MaxConnIdleTime = 5 * time.Minute // 유휴 커넥션 회수 시간
// 2. PostgreSQL 연결
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
log.Fatalf("PostgreSQL 연결 풀 생성 실패: %v", err)
}
defer pool.Close()
// 연결 상태 확인 (Ping)
if err := pool.Ping(ctx); err != nil {
log.Fatalf("PostgreSQL Ping 실패: %v", err)
}
log.Println("🐘 PostgreSQL 16 연결 풀 초기화 성공!")
// 3. sqlc 쿼리 객체 초기화
queries := db.New(pool)
_ = queries // 다음 6편 인증 API에서 바로 주입하여 사용합니다!
}
7. 이번 편 요약 및 다음 단계
오늘 구축한 DB 레이어 덕분에 우리 딩동 메신저는 다음과 같은 강력한 기반을 갖추게 되었습니다:
- 타입 안전한 SQL: 오타나 잘못된 타입 전달로 인한 런타임 서버 다운을 컴파일 단계에서 100% 차단.
- 카톡급 실시간성: 방별 단조 증가
seq를 통해 네트워크가 끊겨도 오차 없이 대화를 복구하는 증분 동기화 토대 마련. - 극저사양 최적화: ORM의 무거운 오버헤드를 완전히 걷어내어 유그린 NAS에서 30MB 이하의 RAM으로 수천 건의 쿼리를 즉각 처리.
다음 **[개발일지 #06]**에서는 이 데이터베이스 위에 초대코드 기반 회원가입, Argon2id 비밀번호 암호화, JWT Access/Refresh 토큰 발급 및 보안 미들웨어를 완성해 보겠습니다!
📂 Part 1. 기획 & 아키텍처 설계
- ✅ [개발일지 #01 | 기획] 유그린 NAS와 Flutter로 만드는 나만의 프라이빗 메신저 ‘딩동’ (기획 & 아키텍처 편)
📂 Part 2. 홈서버 & 인프라 구축 (UGREEN + Synology)
- ✅ [개발일지 #02 | 인프라] Mac에서 Flutter & VS Code 개발 환경 구축하기 (초보자 완벽 가이드)
- ✅ [개발일지 #03 | 인프라] 유그린 NAS에 Caddy & PostgreSQL 구축하기 (홈서버 443 포트 충돌 해결 꿀팁 포함)
📂 Part 3. 백엔드(API) & 실시간 통화 서버
- ✅ [개발일지 #04 | 백엔드] 유그린 NAS에 Go(Golang) 초경량 메신저 백엔드 서버 구축하기
- ⏳ [#05 | 백엔드] JWT 기반 가족 인증 및 실시간 WebSocket 채팅 구현 (예정)
- ⏳ [#06 | 통화] LiveKit 연동으로 무료 음성/영상 통화 서버 구축하기 (예정)
📂 Part 4. 모바일 앱 개발 (iOS / Android)
- ⏳ [#07 | 앱-공통] 모바일 앱 프로젝트 생성 및 로그인/채팅방 UI 구현 (예정)
- ⏳ [#08 | 앱-공통] 300MB 대용량 원본 사진/영상 전송 및 미디어 처리 (예정)
- ⏳ [#09 | 앱-푸시] 앱이 꺼져 있어도 울리는 푸시 알림(FCM/APNs) 연동 (예정)
📂 Part 5. 정식 배포 & 운영
- ⏳ [#10 | 배포] 백엔드 서버 도커 이미지 빌드 및 유그린 NAS 정식 배포 (예정)
- ⏳ [#11 | 운영] DB 자동 백업 스크립트 구성 및 가족 실사용 후기 (예정)