타입스크립트를 쓰면서도 정작 제일 위험한 곳에서 타입이 사라지는 경험, 다들 한 번쯤 있을 것이다. 바로 프런트엔드와 백엔드가 만나는 API 경계다. 서버는 서버대로 타입을 정의하고, 클라이언트는 클라이언트대로 응답 형태를 손으로 다시 적는다. 이 이중 관리를 끝내자는 흐름이 엔드투엔드 타입 안전성(End-to-End Type Safety)이다.
엔드투엔드 타입 안전성은 데이터베이스부터 서버, 네트워크 경계, 클라이언트 화면까지 하나의 타입 정의가 끊기지 않고 이어지게 만드는 접근이다. 어느 한 곳에서 형태가 바뀌면 그걸 쓰는 모든 지점에서 컴파일 단계에 오류가 잡힌다.
서버가 필드 하나를 바꾸면, 클라이언트가 빨간 줄로 먼저 안다.
엔드투엔드 타입은 런타임 사고를 컴파일 오류로 앞당긴다.
API 문서를 눈으로 대조하며 응답 타입을 손으로 베끼던 관행이, 타입을 단일 진실로 공유하는 방식으로 바뀌고 있다.
단일진실 타입 정의를 한 곳에서 |
조기발견 런타임 대신 컴파일 단계 |
자동완성 응답 필드까지 에디터가 안내 |
런타임검증 스키마로 실제 값 확인 |
어디서 타입이 끊기나
문제의 지점은 명확하다. 네트워크를 건너는 순간이다. 서버가 아무리 타입을 잘 정의해도, 그 데이터가 JSON으로 직렬화돼 네트워크를 건너 클라이언트에 도착하면 그냥 정체불명의 값이 된다. 그래서 클라이언트 개발자는 서버 코드나 API 문서를 보고 응답이 어떻게 생겼는지 추정해 타입을 손으로 다시 적는다.
이 수작업이 모든 사고의 씨앗이다. 서버가 어느 날 필드 이름을 바꾸거나 형태를 조정하면? 클라이언트의 손으로 적은 타입은 그대로 남아 있다. 컴파일러는 아무 문제를 못 느끼고, 오류는 사용자가 화면을 열었을 때 런타임에서 터진다. 나는 이런 식으로 배포 후에야 발견되는 버그를 수도 없이 봤다. 근본 원인은 늘 같았다. 타입이 두 곳에 따로 존재했다는 것.
세 갈래의 해법
이 문제를 푸는 접근은 크게 세 갈래로 정리된다. 목표는 같다. 타입 정의를 하나로 만들어 경계 양쪽이 공유하게 하는 것.
- 스키마 우선 — 서버와 클라이언트가 공유하는 스키마를 먼저 정의하고, 그로부터 양쪽 타입을 자동 생성한다. 스키마가 단일 진실이 된다.
- 코드 우선 타입 공유 — 서버의 함수 시그니처와 반환 타입을 클라이언트가 타입 수준에서 직접 가져다 쓴다. 별도 코드 생성 단계 없이 타입만 흐른다.
- 런타임 스키마 검증 — 타입 선언에 그치지 않고, 실제 들어온 값이 그 형태가 맞는지 실행 시점에 검증하는 스키마를 함께 둔다.
세 번째가 특히 중요하다. 타입은 어디까지나 컴파일 단계의 약속일 뿐, 실제 네트워크로 들어온 값이 그 약속을 지킨다는 보장은 없기 때문이다. 외부에서 들어오는 데이터는 런타임 검증을 한 번 거쳐 신뢰할 수 있는 값으로 만드는 게 안전하다.
const User = schema({ id: string(), name: string() });
type User = infer<typeof User>; // 타입은 여기서 파생
const safe = User.parse(response); // 값은 여기서 검증
무엇이 좋아지나
실무 감각으로 말하면, 가장 크게 달라지는 건 리팩터링의 두려움이 사라진다는 점이다. 서버에서 응답 형태를 바꾸면 그걸 쓰는 클라이언트의 모든 지점에 빨간 줄이 즉시 뜬다. 무엇을 고쳐야 하는지 컴파일러가 목록을 만들어 주는 셈이다. 배포하고 나서 사용자 제보로 알게 되는 게 아니라, 코드를 저장하는 순간 안다.
| 항목 | 엔드투엔드 타입 | 수동 타입 관리 |
|---|---|---|
| 타입 출처 | 단일 정의 공유 | 양쪽 각자 작성 |
| 오류 발견 | 컴파일 시점 | 런타임/배포 후 |
| 자동완성 | 응답까지 완전 | 손으로 적은 만큼만 |
| 리팩터링 | 영향 범위 자동 노출 | 수동 추적 |
한계와 선택
물론 전제가 있다. 이 접근은 양쪽이 같은 타입 언어를 쓸 때 가장 매끄럽다. 서버와 클라이언트가 서로 다른 언어로 짜였다면 코드 우선 타입 공유는 어렵고, 스키마 우선이나 코드 생성 방식이 현실적이다. 또 서버와 클라이언트를 강하게 묶는 방식은 편하지만, 여러 외부 소비자가 공개적으로 쓰는 API에는 오히려 명시적인 스키마 계약이 더 맞는다.
결국 핵심은 도구 이름이 아니라 원칙이다. 타입 정의를 두 곳에 두지 말 것. 경계를 건너는 값은 신뢰하지 말고 검증할 것. 이 두 가지만 지키면 API 경계에서 새어 나가던 버그의 상당수가 애초에 생기지 않는다. 나는 이걸 최근 몇 년 프런트엔드 생산성 향상의 가장 조용하지만 확실한 성과로 꼽는다.
자주 묻는 질문
타입스크립트만 있으면 엔드투엔드 타입이 되는 건가요?
아니다. 타입스크립트는 도구일 뿐이고, 서버와 클라이언트가 타입 정의를 실제로 공유하도록 구조를 잡아야 한다. 각자 타입을 따로 적으면 언어가 같아도 경계에서 여전히 끊긴다.
타입이 있으면 런타임 검증은 필요 없지 않나요?
필요하다. 타입은 컴파일 단계의 약속일 뿐, 실제 네트워크로 들어온 값이 그 형태라는 보장은 없다. 외부 데이터는 스키마로 한 번 검증해 신뢰 가능한 값으로 만드는 것이 안전하다.
서버와 클라이언트 언어가 다르면 어떻게 하나요?
공유 스키마를 먼저 정의하고 그로부터 각 언어의 타입을 자동 생성하는 방식이 적합하다. 코드에서 타입을 직접 가져오는 방식은 같은 언어일 때 가장 잘 맞는다.
공개 API에도 이 방식이 좋은가요?
서버와 클라이언트를 강하게 묶는 방식은 한 팀이 양쪽을 함께 개발할 때 유리하다. 불특정 다수가 소비하는 공개 API라면 명시적인 스키마 계약과 버전 관리를 갖추는 편이 더 안정적이다.

댓글 0