✨ v3의 핵심 개선사항
- 🔗 완벽한 조합성: 모든 스펙을 무제한으로 조합 가능
- 🛡️ 타입 안전성: TypeScript의 강력한 타입 시스템 활용
- 🚀 제로 의존성: 외부 라이브러리 없이 독립적으로 동작
- 📦 작은 번들 크기: 최적화된 코드로 빠른 로딩
- 🎯 직관적인 API: 비즈니스 로직을 자연스럽게 표현
- 🔧 유연한 모듈 시스템: CommonJS, ES Modules, UMD 지원
# npm
npm install spec-pattern-ts
# yarn
yarn add spec-pattern-ts
# pnpm
pnpm add spec-pattern-ts
# bun
bun add spec-pattern-ts
import { Spec } from 'spec-pattern-ts'
// 타입 정의
interface User {
age: number
role: string
status: string
}
// 스펙 정의
const isAdult = Spec('user', (u: User) => u.age >= 18)
const isMember = Spec('user', (u: User) => u.role !== 'guest')
const isActive = Spec('user', (u: User) => u.status === 'active')
// 조합
const canAccess = isAdult.and(isMember).and(isActive)
// 사용
const user = { age: 25, role: 'member', status: 'active' }
if (canAccess.is({ user })) {
// 접근 허용
}
import { Spec, CompositeSpec, allOf, anyOf, not } from 'spec-pattern-ts'
// 권한 시스템
const isAdmin = Spec('user', u => u.role === 'admin')
const isOwner = CompositeSpec<{ user: User; resource: Resource }>(
ctx => ctx.resource.ownerId === ctx.user.id
)
// 접근 권한 - 관리자이거나 소유자
const canEdit = isAdmin.or(isOwner)
// 구매 조건 - 로그인하고 성인이며 결제수단이 있어야 함
const canPurchase = allOf(
Spec('user', u => u.isLoggedIn),
Spec('user', u => u.age >= 18),
Spec('payment', p => p.method !== null)
)
// 할인 조건 - VIP이거나 첫 구매이거나 특별 쿠폰 보유
const eligibleForDiscount = anyOf(
Spec('user', u => u.tier === 'vip'),
Spec('order', o => o.isFirstPurchase),
Spec('coupon', c => c.type === 'special')
)
각 스펙은 하나의 비즈니스 규칙만 담당합니다:
const isAdult = Spec('age', (age: number) => age >= 18)
const hasEmail = Spec('email', (email: string) => email.includes('@'))
const isVerified = Spec('verified', (verified: boolean) => verified)
복잡한 규칙은 간단한 스펙들의 조합으로 표현합니다:
// AND 조합
const validUser = isAdult.and(hasEmail).and(isVerified)
// OR 조합
const canComment = isMember.or(isGuest.and(hasEmail))
// NOT 조합
const notBanned = not(isBanned)
// 복잡한 중첩
const complexRule = isAdmin.or(
isMember.and(isVerified).and(notBanned)
)
TypeScript가 필요한 모든 속성을 추적합니다:
const userSpec = Spec('user', (u: User) => u.active)
const productSpec = Spec('product', (p: Product) => p.available)
const combined = userSpec.and(productSpec)
// 타입: ISpecification<{ user: User } & { product: Product }>
// ✅ 올바른 사용
combined.is({ user: myUser, product: myProduct })
// ❌ 컴파일 에러 - product 누락
combined.is({ user: myUser })
interface ISpecification<TContext> {
isSatisfiedBy(candidate: TContext): boolean
is(candidate: TContext): boolean // isSatisfiedBy의 별칭
and<TOther>(other: ISpecification<TOther>): ISpecification<TContext & TOther>
or<TOther>(other: ISpecification<TOther>): ISpecification<TContext | TOther>
not(): ISpecification<TContext>
}
단일 속성을 검증하는 스펙을 생성합니다:
const isExpensive = Spec('price', (price: number) => price > 100)
const isAvailable = Spec('stock', (stock: number) => stock > 0)
여러 속성을 동시에 검증하는 스펙을 생성합니다:
interface OrderContext {
user: User
cart: Cart
payment: Payment
}
const canCheckout = CompositeSpec<OrderContext>(ctx => {
return ctx.user.verified &&
ctx.cart.total > 0 &&
ctx.payment.authorized
})
allOf(...specs)
- 모든 스펙이 만족되어야 함anyOf(...specs)
- 하나 이상의 스펙이 만족되어야 함not(spec)
- 스펙이 만족되지 않아야 함define(predicate).as(key)
- 빌더 패턴으로 스펙 생성
// 제품 구매 가능 여부
const isInStock = Spec('product', (p: Product) => p.stock > 0)
const isPublished = Spec('product', (p: Product) => p.status === 'published')
const isPriceValid = Spec('product', (p: Product) => p.price > 0)
const canBePurchased = allOf(isInStock, isPublished, isPriceValid)
// 할인 적용 조건
const isVIP = Spec('customer', (c: Customer) => c.tier === 'vip')
const isLargeOrder = Spec('order', (o: Order) => o.total > 1000)
const hasPromoCode = Spec('promo', (p: PromoCode) => p.valid)
const discountEligible = anyOf(isVIP, isLargeOrder, hasPromoCode)
// 역할 기반 접근 제어
const hasRole = (role: string) =>
Spec('user', (u: User) => u.roles.includes(role))
const canViewReports = anyOf(
hasRole('admin'),
hasRole('manager'),
hasRole('analyst')
)
const canEditUsers = hasRole('admin')
.and(Spec('user', u => u.permissions.includes('user:write')))
.and(not(Spec('user', u => u.suspended)))
// 콘텐츠 발행 조건
const isAuthor = Spec('user', (u: User) => u.role === 'author')
const isEditor = Spec('user', (u: User) => u.role === 'editor')
const isDraft = Spec('content', (c: Content) => c.status === 'draft')
const isReviewed = Spec('content', (c: Content) => c.reviewedAt !== null)
// 작성자는 초안만, 편집자는 검토된 콘텐츠만 발행 가능
const canPublish = isAuthor.and(isDraft)
.or(isEditor.and(isReviewed))
더 많은 예제는 example 디렉토리를 참고하세요.
- ⚡ 단축 평가: AND/OR 연산에서 불필요한 검증 스킵
- 🎯 최소 객체 생성: 조합 시 오버헤드 최소화
- 🔍 컴파일 타임 타입 체크: 런타임 오버헤드 없음
- 💾 메모리 효율적: 작은 메모리 풋프린트
기여를 환영합니다! 다음과 같은 방법으로 기여할 수 있습니다:
- 버그 리포트 및 기능 제안 (Issues)
- 코드 기여 (Pull Request)
- 문서 개선
- 예제 추가