Argument Validation without Repetition
Quick Summary
Convex와 TypeScript 프로젝트에서 스키마 검증기와 타입 유틸리티를 재사용해 인자 검증 중복을 줄이고, 함수·클라이언트·부분 업데이트까지 일관된 타입 안전성을 유지하는 방법을 설명한다.
🖼️ 인포그래픽
🖼️ 4컷 인포그래픽
💡 한 줄 요약
Convex와 TypeScript 프로젝트에서 스키마 검증기와 타입 유틸리티를 재사용해 인자 검증 중복을 줄이고, 함수·클라이언트·부분 업데이트까지 일관된 타입 안전성을 유지하는 방법을 설명한다.
📌 핵심 요약
- 글은 Convex 앱에서 빠른 개발 반복과 좋은 개발자 경험을 얻으려면 스키마 강제, 인자 검증, 종단 간 타입 힌트가 하나의 흐름으로 맞물려야 한다고 설명한다.
- 스키마 파일에서 데이터 모델 검증기를 정의해 내보내거나, 생성된 schema 객체를 통해 테이블 검증기에 접근하면 같은 데이터 구조를 여러 함수에서 반복 선언하지 않고 재사용할 수 있다.
- 프런트엔드 타입은 Convex가 생성하는 Doc 타입과 WithoutSystemFields 같은 유틸리티로 스키마와 맞출 수 있으며, Pick, Omit, Partial 같은 TypeScript 유틸리티로 필요한 필드 집합만 다룰 수 있다.
- 공유 핸들러가 필요할 때는 ctx.runQuery로 새 하위 트랜잭션을 실행하기보다 일반 TypeScript 함수로 로직을 분리하고, Infer 또는 ObjectType으로 검증기에서 인자 타입을 도출하는 방식을 권장한다.
- convex-helpers는 pick, omit, partial, withSystemFields, doc, typedV 같은 도우미를 제공해 필드 선택, 부분 업데이트, 시스템 필드 포함, 타입 안전한 id 검증을 더 간단하게 만든다.
🧩 주요 포인트
- 글은 Convex 앱에서 빠른 개발 반복과 좋은 개발자 경험을 얻으려면 스키마 강제, 인자 검증, 종단 간 타입 힌트가 하나의 흐름으로 맞물려야 한다고 설명한다.
- 스키마 파일에서 데이터 모델 검증기를 정의해 내보내거나, 생성된 schema 객체를 통해 테이블 검증기에 접근하면 같은 데이터 구조를 여러 함수에서 반복 선언하지 않고 재사용할 수 있다.
- 프런트엔드 타입은 Convex가 생성하는 Doc 타입과 WithoutSystemFields 같은 유틸리티로 스키마와 맞출 수 있으며, Pick, Omit, Partial 같은 TypeScript 유틸리티로 필요한 필드 집합만 다룰 수 있다.
- 공유 핸들러가 필요할 때는 ctx.runQuery로 새 하위 트랜잭션을 실행하기보다 일반 TypeScript 함수로 로직을 분리하고, Infer 또는 ObjectType으로 검증기에서 인자 타입을 도출하는 방식을 권장한다.
- convex-helpers는 pick, omit, partial, withSystemFields, doc, typedV 같은 도우미를 제공해 필드 선택, 부분 업데이트, 시스템 필드 포함, 타입 안전한 id 검증을 더 간단하게 만든다.
🧠 상세 정리
1. 문제의식: 스키마, 검증, 타입 힌트를 한 흐름으로 묶기
이 글은 Convex 앱을 TypeScript로 개발할 때 반복을 줄이면서 타입 안전성을 유지하는 방법을 다룬다. 핵심 문제는 데이터 모델을 스키마에 정의해도 함수 인자 검증, 프런트엔드 타입, 보조 함수 타입을 따로 작성하다 보면 같은 구조가 여러 곳에 반복된다는 점이다. 저자는 스키마 강제, 인자 검증, 종단 간 타입 힌트가 함께 정리될수록 개발 반복 속도와 개발자 경험이 좋아진다고 설명한다. 이전 글에서 다룬 기본 패턴을 바탕으로, 이번 글은 더 발전된 재사용 방식과 도우미 함수를 통해 Convex 프로젝트의 작업 흐름을 가볍게 만드는 데 초점을 둔다.
2. 스키마 정의를 재사용하는 두 가지 방식
첫 번째 재사용 방식은 스키마 파일에서 데이터 모델을 설명하는 검증기 객체를 따로 정의하고, 이를 테이블 정의와 함수 인자 검증에 함께 쓰는 것이다. 예시에서는 recipeFields에 name, course, ingredients, steps 필드를 정의하고, recipes 테이블의 defineTable에도 같은 객체를 사용한다. 그런 다음 addRecipe 같은 mutation의 args에 recipeFields를 그대로 넣어 새 문서를 삽입할 때 동일한 형태를 검증한다. 이렇게 하면 테이블 구조와 함수 인자 구조가 같은 경우 필드 목록을 두 번 작성하지 않아도 되며, 스키마가 데이터 모델의 중심 출처가 된다.
3. schema 객체를 통한 검증기 접근
두 번째 방식은 내보낸 필드 객체를 직접 가져오는 대신 schema 객체에서 테이블 검증기에 접근하는 방법이다. 글은 schema.tables.recipes.validator를 통해 recipes 테이블의 필드 검증기를 얻고, 그 안에서 course 같은 특정 필드 검증기를 꺼낼 수 있음을 보여준다. 이 접근은 이미 정의된 스키마 구조를 기준으로 필요한 검증기를 참조할 수 있다는 점에서 유용하다. 즉, 스키마 파일에 정의된 정보가 테이블 생성에만 쓰이는 것이 아니라, 애플리케이션 전반의 인자 검증에도 다시 활용될 수 있다.
4. 프런트엔드 타입 재사용과 시스템 필드 제외
글은 서버 쪽 검증기 재사용뿐 아니라 프런트엔드 타입도 스키마와 맞춰야 한다고 강조한다. Convex가 생성하는 Doc 타입을 사용하면 특정 테이블의 문서 타입을 클라이언트 코드에서 그대로 참조할 수 있다. 예시에서는 SaveRecipeButton 컴포넌트가 recipeData를 받을 때 WithoutSystemFields<Doc<"recipes">>를 사용해 시스템 필드를 제외한 레시피 데이터를 타입으로 지정한다. 이 방식은 클라이언트에서 mutation을 호출할 때 데이터 모델과 다른 형태의 값을 넘기는 실수를 줄이고, 스키마에 기반한 타입 힌트를 끝까지 이어 준다.
5. 공유 핸들러와 Infer를 이용한 인자 타입 도출
글은 internalQuery의 handler를 mutation에서도 재사용하고 싶을 때 ctx.runQuery를 호출하는 방식을 피하라고 말한다. ctx.runQuery는 새로운 환경에서 별도의 하위 트랜잭션을 실행하므로 대부분의 경우 느리고 큰 이점이 없다는 설명이다. 대신 공통 로직을 일반 TypeScript 함수로 분리하고, query와 mutation이 그 함수를 함께 호출하도록 구성하는 편이 낫다. 이때 v.object로 정의한 인자 검증기에 Infer를 적용하면 검증기에서 TypeScript 인자 타입을 바로 얻을 수 있어, 검증기와 타입을 따로 반복 작성하지 않아도 된다.
6. v.object와 일반 검증기 객체의 타입 추론 차이
저자는 Infer가 모든 형태의 인자 정의에 똑같이 적용되는 것은 아니라는 점도 짚는다. Infer는 v.object처럼 Convex의 검증기 형태로 감싼 값에 사용할 수 있다. 반면 { name: v.string(), course: recipeFields.course }처럼 일반 JavaScript 객체 안에 검증기 필드를 나열한 경우에는 Infer 대신 ObjectType을 사용해야 한다. 이 구분은 실제 Convex 코드에서 args를 작성하는 방식이 여러 가지일 수 있기 때문에 중요하며, 각 방식에 맞는 타입 도출 도구를 선택해야 중복 없이 정확한 타입을 유지할 수 있다.
7. 특정 필드만 선택하거나 제외하는 방법
테이블 전체가 아니라 일부 필드만 필요한 경우에는 TypeScript와 JavaScript 양쪽에서 비슷한 패턴을 사용할 수 있다. 타입 수준에서는 Pick으로 Doc<"recipes">에서 name과 course만 고른 RecipeSummary를 만들거나, Omit으로 course를 제외한 UncategorizedRecipe 타입을 만들 수 있다. 검증기 수준에서는 객체 구조 분해로 recipeFields에서 course만 꺼내 query 인자로 쓰거나, rest 연산자로 course를 제외한 나머지 검증기만 모아 addDessert 같은 mutation에 사용할 수 있다. convex-helpers의 pick과 omit 도우미를 쓰면 이런 선택과 제외 작업을 더 명시적으로 처리할 수 있다.
8. 부분 업데이트를 위한 Partial과 partial 검증기
문서의 일부 필드만 갱신하는 patch 작업에서는 모든 필드가 항상 주어지지 않는다. 타입 수준에서는 TypeScript의 Partial을 사용해 Doc<"recipes">의 모든 필드를 선택적으로 만들 수 있으며, 예시의 updateRecipe 함수는 update 인자로 Partial<Doc<"recipes">>를 받아 db.patch에 전달한다. 검증기 수준에서는 각 필드를 v.optional로 감싼 객체가 필요하지만, 이를 직접 작성하면 반복이 늘어난다. 그래서 convex-helpers/server/validators의 partial 도우미를 사용하면 테이블 검증기 전체를 부분 업데이트용 검증기로 쉽게 바꿀 수 있고, 이 도우미는 일반 객체와 v.object 형태 모두를 받을 수 있다.
9. 시스템 필드와 전체 문서 검증 도우미
기본적으로 schema.tables.recipes.validator.fields에는 _id와 _creationTime 같은 시스템 필드가 포함되지 않는다. 시스템 필드까지 포함한 전체 문서 형태가 필요할 때는 systemFields와 withSystemFields를 사용해 테이블 필드 검증기에 시스템 필드 검증기를 더할 수 있다. 다만 이를 매번 수동으로 조합하는 것은 번거롭기 때문에, 글은 doc 도우미를 사용해 특정 테이블의 모든 필드와 시스템 필드를 한 번에 검증하는 방법도 소개한다. 이 기능은 함수 인자로 완전한 문서 객체를 받아야 하는 경우에 스키마 기반 검증을 더 간단하게 만들어 준다.
10. typedV와 반복 없는 타입 안전성의 결론
마지막으로 글은 typedV를 사용해 스키마에 기반한 더 타입 안전한 v 도구를 만들 수 있다고 설명한다. 예시에서는 typedV(schema)로 만든 vv를 통해 vv.id("recipes")와 vv.doc("recipes")를 사용할 수 있으며, 잘못된 테이블 이름을 넣으면 타입 오류로 잡히는 구조를 보여준다. 단, typedV는 스키마에 의존하므로 스키마를 정의할 때는 기본 v.id를 사용해야 한다는 주의점도 함께 제시된다. 전체 결론은 스키마 검증기, Convex 생성 타입, TypeScript 유틸리티, convex-helpers를 조합하면 인자 검증과 타입 정의의 반복을 크게 줄일 수 있다는 것이다.
🧾 핵심 주장 / 시사점
- 스키마를 단순한 데이터베이스 정의 파일로만 두지 않고, 함수 인자 검증과 클라이언트 타입의 기준점으로 삼는 것이 이 글의 핵심 설계 방향이다.
- ctx.runQuery를 재사용 수단으로 쓰기보다 공통 로직을 일반 함수로 분리하라는 조언은 성능뿐 아니라 트랜잭션 경계와 코드 구조를 함께 고려한 실무적 권장이다.
- Pick, Omit, Partial 같은 TypeScript 기본 유틸리티와 convex-helpers의 검증기 도우미를 짝지어 쓰면 타입 수준과 런타임 검증 수준의 구조를 서로 가깝게 유지할 수 있다.
✅ 액션 아이템
- 스키마 파일에서 데이터 모델 검증기를 정의해 내보내고 생성된 schema 객체로 테이블 검증기를 재사용해 중복 선언을 줄인다.
- 프런트엔드에서는 Convex Doc 타입과 WithoutSystemFields를 맞춘 뒤 Pick/Omit/Partial로 필요한 인자만 허용하도록 타입 범위를 정리한다.
- 공유 로직은 ctx.runQuery 하위 트랜잭션보다 일반 TypeScript 함수로 분리하고 convex-helpers 도우미로 부분 업데이트·id 검증·시스템 필드 처리를 통일한다.
❓ 열린 질문
- Infer 또는 ObjectType으로 유도한 인자 타입과 실제 검증기 스키마가 언제 불일치할 수 있으며, 이를 어떻게 탐지할 것인가?
- 부분 업데이트에서 partial, pick, omit, Omit, Partial를 조합할 때 어떤 규칙으로 필수·선택 필드 경계를 정하면 되는가?
- convex-helpers의 typedV·withSystemFields·doc 사용이 id 안전성과 시스템 필드 일관성 확보에 충분한지를 어떤 실무 지표로 판단할 것인가?