DynamoDB 테이블에서 TypeScript 타입을 생성하는 법
Postgres라면 information_schema를 인트로스펙션해서 타입을 생성했을 겁니다.
DynamoDB에는 그에 해당하는 것이 없습니다 — 그리고 빠진 기능이 아니라 데이터
모델이 그렇습니다: DynamoDB는 항목 스키마를 전혀 저장하지 않습니다. 서비스가
알고 있는 속성은 키에 쓰이는 것들뿐입니다. DescribeTable의
AttributeDefinitions는 스스로의 범위에 대해 명시적입니다: 각 항목은 "테이블과
인덱스 키 스키마의 속성 하나를 설명"합니다
(AWS API 레퍼런스) —
테이블의 나머지 쉰 개 속성은 그 어디에도 기록되어 있지 않습니다.
그래서 "DynamoDB에서 TypeScript 타입 생성"은 언제나 셋 중 하나를 뜻합니다: 형태를 직접 선언하거나, 코드로 작성한 스키마에서 도출하거나, 실제로 존재하는 항목들에서 추론하거나.
DynamoDB 테이블의 TypeScript 타입을 어떻게 얻나요?
테이블의 항목 형태를 반환하는 API는 없습니다 — DescribeTable은 키 속성만
압니다. 선택지는: interface를 손으로 쓰고 경계에서 검증하기(Zod 스키마는 타입과
런타임 검사를 하나의 산출물로 만들어 줍니다), 작성한 스키마가 타입을 낳는
스키마-우선 라이브러리를 쓰기, 아니면 실제 항목에서 형태를 추론하기 —
스크립트로, 또는 테이블을 스캔해서 TypeScript 인터페이스, Zod 스키마, JSON
Schema를 내보내는 DynoTable 같은 도구로요.
방법 1: 인터페이스를 손으로 쓰고 경계에서 검증하기
AWS SDK는 항목의 타입을 대신 정해 주지 못합니다. v3 Document 클라이언트는 항목을
타입 없는 레코드로 반환합니다 — 모든 GetCommand / QueryCommand 결과는 직접
단언하기 전까지 사실상 Record<string, unknown>입니다. 맨몸의 as Order
캐스트는 잘 컴파일되고 런타임에 거짓말을 합니다. 그래서 견고한 버전은
인터페이스를 런타임 검사와 짝짓습니다:
import {z} from 'zod';
const Order = z.object({
PK: z.string(), // ORDER#<id>
SK: z.string(), // META
status: z.enum(['open', 'shipped', 'cancelled']),
total: z.number(),
couponCode: z.string().optional() // sparse attribute
});
type Order = z.infer<typeof Order>;
const {Item} = await doc.send(new GetCommand({TableName: 'Orders', Key: key}));
const order = Order.parse(Item); // typed AND verified스키마 하나, 역할 둘: z.infer는 정적 타입을 주고, parse는 그에 맞지 않는
항목을 잡아냅니다 — 스키마 없는 저장소에서 그것은 _만약_이 아니라 _언제_의
문제입니다. 함정도 똑같이 분명합니다: 스키마는 여러분의 의도를 문서화하지,
테이블을 문서화하지 않습니다. 옛 작성자가 total을 문자열로 저장해 두었다 해도
막을 방법이 없고, 손으로 쓴 타입은 데이터가 진화하며 조용히 표류합니다.
원시(Document 클라이언트가 아닌) API 출력으로 작업한다면, 와이어 형태가 타입
태그가 붙은 DynamoDB-JSON({"S": "..."}, {"N": "123"})이라는 것을 기억하세요 —
마샬링을 보고, 스키마를 쓰는 동안 샘플을
와이어 형태와 일반 형태 사이에서 뒤집어 보려면
DynamoDB JSON 변환기를 쓰세요.
방법 2: 스키마-우선 라이브러리
ElectroDB와 DynamoDB-Toolbox 같은 툴킷은 표류 문제를 쓰기 쪽에서 공략합니다: 코드로 엔터티 스키마를 작성하면, 라이브러리가 TypeScript 타입을 도출하고 동시에 자신이 수행하는 모든 읽기와 쓰기에서 그 형태를 강제합니다. 그것이 얻을 수 있는 가장 강한 보장입니다 — 하지만 방향에 주의하세요: 스키마는 여러분이 씁니다. 라이브러리가 발견해 주지 않습니다. 기존 테이블에 하나를 겨눠도 항목 형태를 먼저 직접 역공학해야 하고, 라이브러리 밖에서 쓰인 항목은 그 보장 밖에 있습니다. 이들은 모든 엔터티가 첫날부터 툴킷을 거치는 그린필드 단일 테이블 설계에서 빛을 발합니다.
방법 3: 실제 항목에서 타입 추론하기
기존 테이블에서는 데이터가 곧 사실입니다. 샘플을 스캔하고, 형태들을 합집합 하세요:
const seen = new Map<string, Set<string>>(); // attr -> observed types
let count = 0;
let key: Record<string, unknown> | undefined;
do {
const page = await doc.send(new ScanCommand({TableName: 'Orders', ExclusiveStartKey: key}));
for (const item of page.Items ?? []) {
count++;
for (const [attr, value] of Object.entries(item)) {
const t = Array.isArray(value) ? 'array' : typeof value;
(seen.get(attr) ?? seen.set(attr, new Set()).get(attr)!).add(t);
}
}
key = page.LastEvaluatedKey;
} while (key && count < 5000);
// emit: attribute -> type union, optional if seen in < count items순진한 버전이 즉시 부딪히는 현실의 함정들:
- 스파스 속성. 한 테이블의 DynamoDB 항목들은 서로 다른 속성을 가질 수
있습니다; 항목의 80%에 존재하는 속성은 없는 게 아니라
optional입니다. 존재 여부만이 아니라 속성별 빈도를 추적하세요. - 섞인 엔터티. 단일 테이블 설계에서는
USER#항목과ORDER#항목이 테이블을 공유합니다 — 둘을 합친 인터페이스 하나는 쓸모없습니다. 타입 속성으로 샘플을 분할하고 엔터티마다 타입 하나를 내보내세요. - 타입 충돌. 같은 속성이 여기서는
N으로, 저기서는S로 저장된 것은 실재하는(그리고 흔한) 데이터 버그입니다 — 조용히 하나를 고르지 말고 유니언으로 드러내세요. 전체 태그 집합은 데이터 타입에 있습니다. - 샘플은 샘플입니다. 드문 항목에만 나타나는 속성은 처음 5,000개 안에 없을 수 있습니다 — 그리고 어느 쪽이든 스캔에는 읽기 용량이 듭니다 (Query 대 Scan).
DynoTable의 원클릭 추론
그 추론 스크립트 — 샘플링, 빈도 추적, 중첩 경로, 엔터티별 분할 — 는 DynoTable의 테이블 통계 패널에 내장되어 있습니다:
- 테이블을 열고 Stats 버튼(막대 차트 아이콘)을 누른 뒤 Index table을
클릭하세요. DynoTable이 실시간 진행률과 함께 테이블을 한 번 스캔하고, 항목이
지닌 모든 속성을 —
commonData.status처럼 점으로 이어진 경로의 중첩 속성까지 포함해 — 그 타입과, 스캔한 행들에서 required였는지 optional이었는지와 함께 찾아냅니다. - Export를 클릭하고 형식을 고르세요:
- TypeScript —
interface. - Zod —
z.object(...)스키마(Standard-Schema 호환). - JSON Schema — draft 2020-12.
- TypeScript —
- 클립보드로 복사하거나 파일로 저장하세요.

내보내기는 자신이 무엇인지에 대해 정직합니다: 생성된 모든 스키마는 샘플링된 항목에서 추론되었다는 메모와 함께 열립니다 — 든든한 출발점이지, 권위 있는 계약이 아닙니다. optional 여부는 인덱싱하는 동안 각 속성이 얼마나 자주 나타났는지를 반영하며, 기본 키 속성은 항상 required로 표시됩니다. 인덱싱에는 일반적인 DynamoDB 읽기 비용이 발생하고, 데이터가 바뀐 뒤에는 Reindex가 그림을 새로 고칩니다.
FAQ
DescribeTable에서 타입을 생성할 수 있나요?
키 속성에 한해서만요. AttributeDefinitions는 테이블과 인덱스 키 스키마를 다룰
뿐 — 항목에 관한 다른 어떤 것도 서비스에 저장되지 않으므로, 인트로스펙션할
서버 측 스키마 자체가 없습니다.
기존 프로덕션 테이블에 타입을 붙이는 가장 좋은 방법은요? 먼저 추론하고, 그다음 굳히세요: 실제 항목을 샘플링해(스크립트 또는 DynoTable의 인덱싱 기반 내보내기) 실제 형태를 얻고, 검토한 뒤, 직접 소유하는 Zod 스키마나 스키마-우선 라이브러리 엔터티로 승격시켜 이후의 표류가 경계에서 잡히게 하세요.
한 테이블에 여러 엔터티 타입이 있으면 어떻게 하나요? 엔터티마다 타입 하나, 절대 합쳐진 타입 하나가 아닙니다. 타입 속성(또는 키 접두사)으로 샘플을 나누고 각각에 대해 별도 인터페이스를 생성하세요 — 그것들의 판별 유니언이 여러분의 테이블 타입입니다.
왜 생성된 타입이 필수 필드를 optional이라고 하나요? 샘플링된 어떤 항목에 그 필드가 없었기 때문입니다. 스키마 없는 저장소에서 optional 여부는 선언이 아니라 관찰입니다 — 그 항목들이 백필해야 할 레거시 행인지(마이그레이션 참고), 아니면 정말로 선택적인 속성인지 확인하세요.
타입이 DynamoDB의 세트와 바이너리도 다루나요? 변환기는 일반-JSON 표현을 선택해야 합니다: 세트는 배열이 되고 바이너리는 인코딩된 문자열이 됩니다 — 마샬링에서 다루는 것과 같은 매핑상의 기벽입니다. 샘플을 DynamoDB JSON 변환기로 왕복시켜 속성이 양쪽에서 정확히 어떻게 보이는지 확인하세요.
테이블의 형태를 그만 짐작하세요 — DynoTable을 다운로드하고, 테이블을 인덱싱하고, TypeScript, Zod, JSON Schema를 클릭 한 번으로 내보내세요.


