메인 콘텐츠로 건너뛰기
Sonamu는 Entity 정의 하나로 클라이언트부터 서버, 데이터베이스까지 완벽한 타입 안전성을 제공합니다. 이 문서는 타입이 어떻게 전체 스택을 관통하는지 설명합니다.

타입 흐름 개요

컴파일 타임

TypeScript로 타입 체크 개발 중 에러 방지

런타임

Zod로 실제 데이터 검증 잘못된 데이터 차단

데이터베이스

PostgreSQL 제약조건 데이터 무결성 보장

API 경계

Request/Response 검증 안전한 데이터 교환

레이어별 타입 안전성

1. Entity → TypeScript

Entity 정의가 TypeScript 타입으로 변환됩니다.
{
  "id": "User",
  "props": [
    { "name": "email", "type": "string", "length": 100 },
    { "name": "age", "type": "integer", "nullable": false },
    { "name": "role", "type": "enum", "id": "UserRole" }
  ],
  "enums": {
    "UserRole": {
      "admin": "관리자",
      "normal": "일반 사용자"
    }
  }
}
타입 안전성 보장:
// ✅ 컴파일 성공
const user: User = {
  email: "[email protected]",
  age: 30,
  role: "admin",
};

// ❌ 컴파일 에러
const user: User = {
  email: "[email protected]",
  age: "30", // Error: Type 'string' is not assignable to type 'number'
  role: "guest", // Error: Type '"guest"' is not assignable to type 'UserRole'
};

2. Entity → Zod 스키마

Entity 정의가 Zod 스키마로 변환되어 런타임 검증을 제공합니다.
export const User = z.object({
  email: z.string().max(100),
  age: z.number().int(),
  role: UserRole,
});

3. Entity → PostgreSQL

Entity 정의가 PostgreSQL 테이블 스키마로 변환됩니다.
{
  "props": [
    { "name": "email", "type": "string", "length": 100 },
    { "name": "age", "type": "integer", "nullable": false }
  ],
  "indexes": [
    { "type": "unique", "columns": [{ "name": "email" }] }
  ]
}
데이터베이스 제약조건:
-- ✅ 삽입 성공
INSERT INTO users (email, age) VALUES ('[email protected]', 30);

-- ❌ 실패: NULL 불가
INSERT INTO users (email, age) VALUES ('[email protected]', NULL);

-- ❌ 실패: UNIQUE 위반
INSERT INTO users (email, age) VALUES ('[email protected]', 25);

API 레이어 타입 안전성

Model → API Client

Model의 메서드가 타입 안전한 API 클라이언트로 변환됩니다.
@api({ httpMethod: "POST" })
async save(params: UserSaveParams[]): Promise<number[]> {
  // 구현...
}

Request 검증

API 요청은 Zod 스키마로 자동 검증됩니다.
Fastify 라우터에서
// Sonamu가 자동으로 생성
fastify.post("/user/save", async (request, reply) => {
  // 자동 검증
  const params = UserSaveParams.array().parse(request.body.params);

  // 검증된 타입으로 안전하게 사용
  const result = await UserModel.save(params);
  return result;
});
검증 실패 시:
// Request
POST /user/save
{
  "params": [
    { "email": "invalid-email", "age": "30" }
  ]
}

// Response (400 Bad Request)
{
  "error": "Validation failed",
  "issues": [
    {
      "path": ["params", 0, "email"],
      "message": "Invalid email"
    },
    {
      "path": ["params", 0, "age"],
      "message": "Expected number, received string"
    }
  ]
}

Response 타입

API 응답도 타입 안전합니다.
@api({ httpMethod: "GET" })
async findById(id: number): Promise<User> {
  return this.findById("A", id);
}

TanStack Query 타입 안전성

TanStack Query hooks도 완벽한 타입 안전성을 제공합니다.
export function useUserById(id: number) {
  return useQuery({
    queryKey: ["User", "findById", id],
    queryFn: () => findUserById(id),
  });
}

export function useSaveUser() {
  return useMutation({
    mutationFn: (params: UserSaveParams[]) => saveUser(params),
  });
}

Subset 타입 안전성

Subset 쿼리도 완벽한 타입 추론을 제공합니다.
async findMany<T extends UserSubsetKey>(
  subset: T,
  params?: UserListParams
): Promise<ListResult<UserListParams, UserSubsetMapping[T]>> {
  const { qb } = this.getSubsetQueries(subset);
  
  // subset에 따라 타입이 자동으로 결정됨
  return this.executeSubsetQuery({ subset, qb, params });
}

전체 스택 타입 흐름 예시

실제 CRUD 작업에서 타입이 어떻게 흐르는지 보여줍니다.
1

1. Entity 정의

user.entity.json
{
  "id": "User",
  "props": [
    { "name": "email", "type": "string", "length": 100 },
    { "name": "age", "type": "integer" },
    { "name": "role", "type": "enum", "id": "UserRole" }
  ],
  "subsets": {
    "A": ["id", "email", "age", "role"]
  }
}
생성: TypeScript 타입, Zod 스키마, PostgreSQL 스키마
2

2. Model 작성

user.model.ts
class UserModelClass extends BaseModelClass<...> {
  @api({ httpMethod: "POST" })
  async save(params: UserSaveParams[]): Promise<number[]> {
    // 타입 안전한 구현
    params.forEach(p => wdb.ubRegister("users", p));
    return wdb.ubUpsert("users");
  }
}
생성: API 클라이언트, TanStack Query hooks
3

3. 프론트엔드 사용

UserForm.tsx
import { useSaveUser } from "@/services/services.generated";
import { UserSaveParams } from "@/services/user/user.types";

function UserForm() {
const { mutate } = useSaveUser();

const handleSubmit = (values: UserSaveParams) => {
mutate([values]); // ✅ 타입 체크됨
};

return <form>...</form>;
}

검증: 컴파일 타임 (TypeScript) + 런타임 (Zod)
4

4. API 요청

// Request (자동 직렬화)
POST /user/save
{
  "params": [
    { "email": "[email protected]", "age": 30, "role": "admin" }
  ]
}

// Zod 자동 검증
const params = UserSaveParams.array().parse(request.body.params);
검증: Zod 스키마로 런타임 검증
5

5. 데이터베이스 저장

-- 자동 생성된 쿼리
INSERT INTO users (email, age, role)
VALUES ('[email protected]', 30, 'admin')
RETURNING id;

-- PostgreSQL 제약조건 체크
-- - email: VARCHAR(100) NOT NULL
-- - age: INTEGER NOT NULL
-- - role: CHECK (role IN ('admin', 'normal'))

검증: PostgreSQL 제약조건
6

6. 응답 반환

// Response
{ "data": [1] }  // number[]

// 프론트엔드에서 타입 추론
const ids = await saveUser([...]);
// ids: number[]
타입 추론: API 클라이언트가 반환 타입 보장

타입 불일치 방지

Sonamu의 E2E 타입 안전성은 흔한 타입 불일치 문제를 방지합니다.
문제: API가 예상과 다른 타입 반환Sonamu 해결:
// Model에서 반환 타입 명시
@api()
async findById(id: number): Promise<User> {
  return this.findById("A", id);
}

// API 클라이언트도 같은 타입
export async function findUserById(id: number): Promise<User> {
  // ...
}

// 타입 불일치 시 컴파일 에러
문제: 클라이언트가 잘못된 파라미터 전송Sonamu 해결:
// 동일한 타입 정의 공유
export const UserSaveParams = z.object({...});

// Model에서 사용
async save(params: UserSaveParams[]): Promise<number[]>

// API 클라이언트에서 사용
export async function saveUser(params: UserSaveParams[]): Promise<number[]>

// 서버에서 자동 검증
const validated = UserSaveParams.array().parse(request.body.params);
문제: 하드코딩된 문자열이 Enum과 불일치Sonamu 해결:
// ❌ 잘못된 방법
const role = "guest";  // 컴파일은 되지만 런타임 에러

// ✅ 올바른 방법
import { UserRole } from "./sonamu.generated";
const role: UserRole = "admin";  // 타입 체크됨
문제: nullable 필드를 null 체크 없이 사용Sonamu 해결:
type User = {
  bio: string | null;  // nullable 명시
};

// ❌ 컴파일 에러
const length = user.bio.length;

// ✅ null 체크 강제
const length = user.bio?.length ?? 0;

타입 안전성 검증

타입 안전성이 제대로 작동하는지 확인하는 방법입니다.

컴파일 타임 검증

// 타입 테스트 파일 생성
import { expectType } from "tsd";
import type { User, UserSaveParams } from "./user.types";

// 타입 체크
expectType<User>({
  id: 1,
  email: "[email protected]",
  age: 30,
  role: "admin",
});

// ❌ 이 코드는 컴파일 에러를 발생시켜야 함
expectType<User>({
  id: 1,
  email: "[email protected]",
  age: "30", // Type error
});

런타임 검증

import { describe, it, expect } from "vitest";
import { User, UserSaveParams } from "./user.types";

describe("User type validation", () => {
  it("should validate correct user", () => {
    const result = User.safeParse({
      email: "[email protected]",
      age: 30,
      role: "admin",
    });

    expect(result.success).toBe(true);
  });

  it("should reject invalid user", () => {
    const result = User.safeParse({
      email: "invalid-email",
      age: "30",
      role: "guest",
    });

    expect(result.success).toBe(false);
  });
});

다음 단계