본문 바로가기

TypeScript에서의 API: 프로덕션 레디 Typed API 빌드 방법

타입 지정 DTO, 검증, 클라이언트, 프로덕션 모범 사례를 활용해 TypeScript로 API를 스캐폴딩부터 배포까지 구축하는 방법을 알아보세요.

TypeScript에서의 API: 프로덕션 레디 Typed API 빌드 방법

당신의 TypeScript API는 출시 당일에는 꽤 견고해 보였을 겁니다. 라우트가 컴파일되고, 프론트엔드가 공유된 타입을 가져오며, 에디터가 모두에게 '안전하게 배포해도 된다'는 느낌을 주는 그 깔끔한 초록색 표시가 떴을 테니까요.

그런데 백엔드가 응답 필드를 하나 바꾸거나, 예상치 못한 곳에 nullable 값이 나타나거나, 모바일 클라이언트가 여전히 구형 페이로드 형식을 호출하는 경우가 생깁니다. 대부분의 TypeScript API 작업이 깨지는 지점은 바로 여기입니다. 문법이 아니라, 드리프트(drift) 때문이죠.

목차

타입이 지정된 API가 출시 후 실패하는 이유와 방지 방법

타입이 지정된 API은 보통의 릴리스 중에 깨지기 쉽습니다. 한 팀은 응답 필드를 이름 변경하고, 다른 팀은 부분 마이그레이션을 위해 nullable 분기를 추가합니다. 모바일 업데이트가 웹보다 늦기 때문에 오래된 클라이언트는 이전 페이로드를 계속 전송합니다. 로컬 타입을 업데이트한 모든 저장소에서 TypeScript는 여전히 컴파일됩니다. 하지만 프로덕션의 계약은 이미 거짓이 됩니다.

이러한 실패에는 이름이 있습니다: 계약 드리프트(contract drift).

초기 출시 후 프로덕션 환경에서 타입이 지정된 API가 실패하는 세 가지 주요 이유를 설명하는 다이어그램.

TypeScript는 API 작업을 더 쾌적하게 만들었지만, 약한 계약에 대한 과도한 신뢰도 쉽게 만들었습니다. 공유 인터페이스, 라우트 제네릭, 그리고 타입이 지정된 fetch 래퍼는 개발 단계에서 도움이 됩니다. 하지만 두 번째나 열 번째 릴리스 이후에도 네트워크를 건너오는 JSON이 여전히 해당 타입과 일치한다는 것을 증명하지는 못합니다.

프로덕션에서 유지되는 규칙은 단순합니다.

검증되지 않은 JSON이 애플리케이션 로직으로 바로 흘러들어갈 수 있다면, 당신의 TypeScript 타입은 의도를 설명하는 것이지 현실이 아닙니다.

해결책은 교묘한 타입 기예보다는 진실이 어디에 존재하는지에 더 가깝습니다:

  • 경계에서 검증하세요. 요청 본문, 파라미터, 헤더, 심지어 다운스트림 서비스 응답까지도 나머지 code가 그들을 다루기 전에 파싱하세요.
  • DTO를 도메인 모델로 매핑하세요. transport shape을 비즈니스 객체와 분리해 두어 API의 잦은 변경이 코드베이스 전체로 새어 나가지 않게 하세요.
  • 컨트랙트에서 타입을 생성하세요. OpenAPI, JSON Schema, 또는 schema-first 프레임워크를 쓰면 클라이언트와 서버가 같은 단일 소스(source of truth)를 공유하게 됩니다.
  • Breaking change는 공개 이벤트로 취급하세요. 필드 구조가 바뀐다면 의도적으로 버전을 붙이고, 다른 외부 컨트랙트 변경과 마찬가지로 이를 알리세요.

DTO 매핑은 팀이 가장 자주 건너뛰는 부분입니다. 처음엔 불필요하게 느껴지죠. 하지만 릴리스가 몇 번 지나면, 다음을 막아주는 계층이 됩니다. string | null 레거시 필드 별칭이 모든 서비스와 프론트엔드 화면으로 퍼지는 것 말입니다. 경계에서 하는 작은 변환 단계가 나중에 대규모 리팩터링을 하는 것보다 훨씬 저렴합니다.

Typed API가 실패하는 또 다른 이유는 에러 컨트랙트가 늘 뒷전이기 때문입니다. 성공 페이로드에는 신경을 쓰지만, 실패 페이로드는 그날 던져진 예외가 직렬화된 모습 그대로가 됩니다. 그러면 클라이언트는 설계된 적 없는 구조 위에 재시도 로직, 사용자 메시지, 모니터링을 쌓게 됩니다. 결과는 같은 문제가 다른 형태로 나타나는 것입니다. 바로 Drift죠.

버전 관리에도 같은 원칙이 필요합니다. 팀이 단 한 번의 대대적인 재작성으로 클라이언트를 깨뜨리는 경우는 드뭅니다. 합리적으로 보이는 국소 변경이 쌓여 호환성이 깨지면서 클라이언트가 망가지죠. 명확한 API 진화하는 컨트랙트를 위한 버전 관리 전략은 변경 사항이 소비자에게 닿기 전에 미리 보이게 해줍니다.

목표는 TypeScript를 모든 곳에 사용하는 것이 아닙니다. 목표는 출시 후, 여러 배포와 클라이언트, 실제 프로덕션 데이터가 첫날의 깔끔한 타입에 압박을 가할 때에도 계약이 진실되게 유지되도록 하는 것입니다.

TypeScript API 프로젝트를 올바르게 스캐폴딩하기

타입이 지정된 API는 첫날에는 깔끔해 보입니다. 6개월 후, 한 라우트는 검증되지 않은 입력을 허용하고, 다른 라우트는 원시 데이터를 읽습니다. process.env, 그리고 세 번째는 클라이언트가 코딩하지 않은 형태를 반환합니다. 스캐폴드가 한 번에 깨지는 경우는 드뭅니다. 계약 드리프트가 일상적인 기능 개발에 스며들 만큼의 여지를 만듭니다.

계약을 우회하기 어려운 프로젝트 구조로 시작하세요.

IDE 터미널에서 TypeScript 오류가 표시되는 노트북 화면에서 code를 입력하는 개발자.

팀 구조에 맞는 프레임워크 선택

TypeScript 기반 API의 경우, 첫 번째 프레임워크 결정은 구문보다 계약 규율이 어디에 위치할지가 더 중요합니다.

  • Express 최소한의 추상화와 이미 미들웨어 모델을 잘 아는 팀에 적합합니다. 모든 라우트가 자체 검증, 오류 형태, 응답 규약을 발명하기 전까지는 방해가 되지 않습니다.
  • Fastify 소규모 및 중규모 백엔드 팀의 강력한 기본값입니다. 플러그인 시스템이 깔끔하고, 스키마 작업을 라우어 레이어에 가깝게 배치하여 런타임 동작이 타입과 일치하도록 돕습니다.
  • Nest 많은 기여자, 공유 모듈, 명시적인 소유권 경계가 있는 대규모 코드베이스에 잘 맞습니다. 비용은 의식(세레모니)이며, 서비스 자체가 작다면 이 비용은 현실적입니다.

보통 팀이 사용할 만큼의 프레임워크만 도입합니다. Fastify, 검증 라이브러리, 생성된 계약 타입으로 구성된 작은 서비스는 일관되지 않은 규약이 쌓인 더 무거운 스택보다 리팩토링을 더 잘 견디는 경우가 많습니다.

경계를 보호하는 폴더 구조 사용하기

폴더 이름보다 import 구조가 더 중요합니다. 라우트가 데이터베이스 모델에 직접 접근하거나, 서비스가 ORM 엔티티를 클라이언트에 그대로 반환한다면, 그 스캐폴드 자체가 코드 드리프트를 초래하고 있는 것입니다.

프로덕션 환경에서 견고하게 유지되는 레이아웃은 보통 전송(transport) 관련 관심사와 애플리케이션 관련 관심사를 분리합니다:

  • src/routes HTTP 연결 전용
  • src/schemas 요청 및 응답 스키마용
  • src/dto 전송 타입 및 매핑용 code
  • src/services 유스케이스 및 오케스트레이션용
  • src/domain 단일 엔드포인트를 넘어 지속되어야 하는 비즈니스 모델용
  • src/clients 하위 시스템 통합용
  • src/errors 공통 에러 타입과 narrowing 헬퍼용
  • src/config 시작 시 설정 파싱용

그 src/dto 레이어는 단순 반복 작업이 아닙니다. API가 외부 변경을 흡수할 자리를 제공하되, 그 변경이 도메인 로직으로 새어 들어가거나 무관한 엔드포인트를 통해 다시 나가지 않게 합니다.

설정도 같은 방식으로 다뤄야 합니다. 시작 시 환경 변수를 한 번만 파싱하고, 잘못된 값은 즉시 실패하게 하며, 타입이 지정된 config 객체를 앱 나머지 부분에 export하세요. 핸들러 내부에서 계속 process.env 데이터를 읽는 팀은 결국 TypeScript가 도와줄 수 없는 분기 많은 런타임 동작을 마주하게 됩니다. 이 환경 설정 가이드는 그 패턴을 표준화해야 한다면 좋은 참고 자료입니다.

기능을 추가하기 전에 컴파일러를 더 엄격하게 설정하세요

프로덕션 API는 안전하지 않은 code을 작성하기 번거롭게 만들어야 합니다.

유용한 기본값은 다음과 같습니다:

  • strict 일시 중지되지 않은 동안
  • useUnknownInCatchVariables 일시 중지되지 않은 동안
  • noUncheckedIndexedAccess 팀이 추가적인 규율을 관리할 수 있을 때 활성화
  • Node, 테스트, 번들링, 툴링이 모두 동일한 방식으로 해석하지 않는 한 경로 별칭 사용 금지
  • 분리 build, typecheck, 그리고 CI에서 린트 스크립트 실행

허술한 tsconfig 설정이라면 드리프트가 눈치채지 못한 채 쌓입니다. 엄격한 설정은 불일치가 프로덕션 문제로 번지기 전에 가시적인 작업으로 전환합니다.

린트 규칙도 도움이 되며, 특히 any, 플로팅 프로미스, 공개 계약 모듈에서의 우발적 내보내기를 금지하는 규칙이 그렇습니다. 이것만으로 런타임 검증의 대체재는 아니지만, 계약 실수가 숨을 수 있는 지점을 줄여줍니다.

초기에 중요한 스캐폴딩 선택 사항이 하나 더 있습니다. OpenAPI 스펙의 출처를 결정하고, 그 결정을 라우트 레이어에 가깝게 유지하세요. 일부 팀은 code-퍼스트 스키마에서 생성합니다. 다른 팀은 스펙을 기반으로 서버 스텁과 타입을 먼저 생성합니다. 두 방식 모두 가능합니다. 실패하는 경우는 첫 릴리스 이후 아무도 확인하지 않는 부수 산출물로 스펙을 대하는 것입니다.

초기 스캐폴딩 이후, 순수한 요청-응답 서비스 밖에서 타입 지정된 계약이 어떻게 동작하는지 비교해 보는 것이 도움이 됩니다. Streamkap Flink TypeScript 가이드 스트림이나 이벤트 중심 시스템을 다루는 팀에게 유용하며, 여기서 계약 드리프트는 HTTP 핸들러뿐 아니라 더 긴 파이프라인 전반에 걸쳐 나타납니다.

경계에서의 DTO 설계 및 입력값 검증

타입 지정된 API은 보통 첫날에는 올바르게 보입니다. 6개월 후, 버그는 경계에서 드러납니다. 모바일 클라이언트가 여전히 오래된 필드를 보내고, 파트너가 프론트엔드가 항상 존재한다고 가정했던 속성을 생략하며, 리팩토링이 내부 ORM 컬럼을 공개 응답에 노출시킵니다. TypeScript는 코드베이스 안에서 제 역할을 했습니다. 하지만 계약은 여전히 드리프트되었습니다.

그래서 DTO 설계가 중요합니다. 요청 본문을 깔끔하게 만드는 게 목적이 아닙니다. 첫 릴리스 이후에도 공개 타입이 실제와 어긋나지 않게 유지하는 것이 핵심입니다.

Public contract와 내부 모델은 같으면 안 됩니다

어떤 DTO 는 네트워크를 오가는 데이터를 설명합니다. 도메인 모델 은 애플리케이션이 실제 작업을 수행하는 데 필요한 것을 정의합니다. 이 두 관심사를 합치면 초반에는 몇 줄을 아낄 수 있지만, 나중에는 비용이 큰 결합을 만듭니다.

안전한 시스템 경계와 API 계약 유지를 위한 DTO 설계 및 검증 과정을 보여주는 다이어그램.

라우트가 이 데이터를 수신하면:

type CreateOrderRequestDto = {
  customerId: string
  items: Array<{ sku: string; quantity: number }>
  note?: string | null
}

서비스 레이어는 여전히 다음과 같이 더 좁고 깔끔한 타입을 받아야 합니다. OrderDraft 정규화된 문자열, 검증된 수량, 한 곳에서 적용된 기본값을 포함합니다.

경계 계층에서는 보통 다음 단계가 필요합니다:

  1. 인바운드 페이로드 파싱
  2. 데이터 구조 및 필드별 제약 조건 검증
  3. DTO를 도메인 객체로 매핑
  4. 도메인 객체에 비즈니스 로직 실행
  5. 결과를 응답 DTO로 매핑
  6. 전송 전 아웃바운드 응답 검증

6번째 단계는 자주 건너뛰어집니다. 하지만 이 단계는 private 필드, 안정적인 응답에 새어 나온 nullable 값, 리팩터링 중 발생한 의도치 않은 스키마 변경을 잡아내는 단계이기도 합니다.

비즈니스 로직이 데이터를 건드리기 전에 검증하세요

컴파일 타임 타입은 네트워크에서 오는 JSON을 검증하지 못합니다. 또한 다른 서비스가 여전히 조건을 만족하는 구조를 반환해도 보호해 주지 못합니다. unknown 런타임에서 여러분의 가정을 깨뜨립니다.

TypeScript에서 API 작업을 한다면 Zod가 흔한 선택입니다. 런타임에 파싱하고 나머지 code의 타입을 추론해 주기 때문입니다. Valibot, io-ts 같은 라이브러리도 쓸 수 있습니다. 중요한 건 라이브러리가 아니라 규칙입니다. 신뢰할 수 없는 데이터는 다른 무엇이 쓰기 전에 먼저 파싱해야 합니다.

리팩토링 후에도 살아남는 패턴은 이렇습니다:

  • Inbound 스키마 잘못된 형식의 요청 데이터 거부
  • Dependency 스키마 서드파티 API와 내부 서비스의 응답 검증
  • Outbound 스키마 API이(가) 발행하려는 응답 검증

많은 타입 기반 API가 출시 후 무너지는 지점이 바로 이 중간 계층입니다. 팀들은 요청은 검증하면서 다운스트림 응답 검증은 건너뛰고, 그러고 나서 벤더가 필드명 하나를 바꾼 게 왜 운영 장애로 이어지는지 의아해합니다.

제가 쓰는 실무 규칙은 이렇습니다. Raw JSON은 라우트 계층에서 멈춥니다.

code 매핑은 낭비가 아닙니다. 드리프트가 드러나는 지점입니다

팀은 DTO 매핑이 반복적이라고 느껴 기피하는 경우가 많습니다. 하지만 저는 프로덕션에서 정반대를 봤습니다. 얇은 매핑 레이어가 있으면 contract 변경이 드러나고, 리뷰하기 쉬우며, 변경 범위를 국소화할 수 있습니다.

예를 들어:

  • transport에서 허용 note?: string | null
  • 도메인 모델에 저장 가능 note: string 포함 "" 기본값으로
  • 응답 DTO에서 생략 가능 note 비어 있으면 완전히 생략

이는 서로 다른 세 대상에게 각각 필요한 세 가지 진실입니다. 이를 하나의 공유 인터페이스로 취급하면 클라이언트가 깨지기 전까지 그 차이가 숨겨집니다.

webhook은 소비자가 페이로드 형태를 수년간 유지할 수 있어 이 점이 더 분명해집니다. 팀에서 이 문제를 다루고 있다면, 이 webhook 페이로드 설계 예시 유용한 참고 자료입니다.

공유 타입은 Single Source of Truth가 명확할 때만 유용합니다

백엔드 인터페이스를 프론트엔드로 복사하는 건 시차를 두고 찾아오는 드리프트입니다. 공유 패키지가 도움이 될 수 있지만, 의도적으로 공개한 타입에만 해당됩니다.

대규모 코드베이스에서 더 잘 버티는 구조는 이렇습니다:

  • public request/response 스키마는 persistence 모델과 별도로 정의합니다
  • 해당 공개 스키마에서 OpenAPI를 생성하거나, OpenAPI에서 서버 타입을 먼저 생성합니다
  • 생성된 contract 타입은 핸들러와 클라이언트 가까이에 둡니다
  • 도메인 타입과 ORM 모델은 내부에만 유지합니다
  • 호환성이 중요한 경우, 공개 DTO의 버전을 의도적으로 관리합니다

이러한 분리는 다음 가이드라인과도 일치합니다: Azure SDK 팀의 TypeScript 설계 가이드라인, 이 가이드라인은 안정적인 공개 인터페이스를 유지하고, 내부 구현 세부 사항이 contract에 노출되지 않도록 강조합니다.

유지보수하기 좋은 경계는 의도적으로 심심해 보입니다

좋은 버전은 덜 영리합니다.

이전에는 프론트엔드가 신뢰합니다 fetch().json() 마치 진실인 양, 백엔드는 ORM 객체를 직접 반환하고, 하나의 공유 인터페이스가 모든 레이어를 표현하려 합니다. 이후에는 각 경계에서 데이터를 파싱하고, DTO는 좁게 유지하며, 도메인 모델은 내부에 둡니다. 생성된 타입은 공개 컨트랙트를 포괄하고, code 매핑이 변경 사항을 명시적으로 만듭니다.

절차가 늘어납니다. 동시에 호출자가 발견하기 전에 불일치를 검토할 한 곳이 생깁니다.

완전한 타입이 적용된 API 클라이언트 생성 및 사용

타입이 적용된 클라이언트는 출시 당일에는 종종 완성된 것처럼 보입니다. 3개월 뒤, 한 엔드포인트는 nullable 필드를 반환하기 시작하고, 다른 엔드포인트는 커서 페이지네이션을 추가하며, 모바일 앱은 이전 버전의 컨트랙트를 고정합니다. TypeScript 타입은 여전히 컴파일됩니다. 호출부는 여전히 깨집니다.

이것이 바로 클라이언트 레이어의 역할입니다. 단순히 에디터의 자동 완성을 좋게 만드는 것이 아니라, 첫 출시 이후에도 공개된 컨트랙트의 신뢰성을 유지해야 합니다.

타입 클라이언트 전략 선택하기

클라이언트 구조는 팀의 선호도가 아니라 API의 실제 복잡도에 맞춰야 합니다.

접근 방식 추천 대상 트레이드오프
직접 구현한 fetch wrapper 소규모 앱, 특이한 인증 흐름, 빠른 반복 개발 시작은 빠르지만 시간이 지나면 호출부마다 파편화되기 쉬움
OpenAPI code 생성 안정적인 스키마를 갖춘 단순한 REST API 기본기는 탄탄함. 커스텀 인증, 스트리밍, 특이한 페이지네이션에는 추가 작업 필요
SDK 스타일의 타입 지정 클라이언트 여러 팀이 쓰는 플랫폼, 공개 API, 장기간 유지되는 통합 유지보수 비용이 가장 높음. API 자체가 제품일 때 최상의 사용자 경험 제공

직접 구현한 클라이언트는 작은 규모의 서비스에 적합함

커스텀 fetch wrapper는 API이 내부용이거나, 서비스 규모가 작거나, 스키마 생성보다 전송 동작이 더 중요할 때 합리적인 선택입니다. 저도 관리자 도구나 초기 단계 서비스에는 여전히 이 방식을 사용합니다.

실패 모드는 드리프트입니다. 한 팀은 래퍼에 재시도 규칙을 추가하고, 다른 팀은 이를 우회하며, 또 다른 팀은 응답 타입을 프론트엔드로 복사해 타입을 확장해 버립니다. any 첫 불일치 이후에는 서버가 반환하는 값을 더 이상 나타내지 못하는 ‘typed’ 호출만 남게 됩니다.

다음 조건이 맞다면 직접 만든 클라이언트를 사용하세요:

  • API 규모가 작고 내부용인 경우
  • 계약이 자주 바뀌어 code 재생성이 노이즈가 될 정도인 경우
  • 커스텀 트랜스포트 동작이 작업의 대부분을 차지하는 경우
  • TypeScript 어노테이션만 두지 않고 클라이언트에서 런타임 파싱을 유지할 의향이 있는 경우

마지막 포인트가 중요합니다. response.json() 함수 시그니처와 달리 런타임에서는 unknown 데이터를 반환합니다.

OpenAPI 생성이 실용적인 기본값입니다

안정적인 REST API라면 생성된 타입이 유지보수성과 안전성의 균형이 가장 좋습니다. 중복 타입 작성을 크게 줄이고, 계약 변경을 pull request에서 바로 볼 수 있게 해줍니다.

리팩토링을 견디는 패턴은 간단합니다. 공개 계약에서 타입을 생성하고, 생성된 레이어는 얇게 유지하며, 소비자가 더 나은 사용성을 필요로 하는 곳에만 작은 래퍼를 추가하세요. OpenAPI TypeScript 생성 워크플로우 해당 모델에 잘 맞습니다.

유용한 분리 방식은 다음과 같습니다:

  • 생성된 code가 요청과 응답 형태를 담당합니다
  • 가벼운 SDK 래퍼가 인증 주입, 재시도, 페이지네이션 헬퍼를 담당합니다
  • 런타임 검증은 여전히 서버 경계와 신뢰할 수 없는 입력이 시스템에 다시 들어오는 모든 곳에서 수행됩니다
  • DTO 매핑은 명시적으로 유지해서 내부 모델 변경이 클라이언트 계약으로 새어 나가지 않도록 합니다

이 하이브리드 접근 방식은 생성된 code를 단순하게 유지하는데, 이게 좋습니다. 단순한 code일수록 다시 생성하고 검토하고 교체하기 쉽습니다.

애플리케이션 code이 손대기 전에 생성된 클라이언트를 래핑하세요

생성된 함수는 보통 코드베이스 전반에서 폭넓게 쓰기엔 너무 로우 레벨입니다. 그러면 모든 호출자가 전송 세부 사항을 다시 익혀야 합니다.

가벼운 래퍼를 두면 정책을 일관되게 유지할 한 곳이 생깁니다:

  • 기본 헤더와 request ID 추가
  • 에러 형태 정규화
  • 페이지네이션을 iterator 또는 헬퍼 메서드로 노출
  • 멀티 테넌트 환경을 위한 요청별 인증 오버라이드 지원
  • 생성된 요청 및 응답 타입을 직접 다시 작성하지 말고 그대로 유지

예를 들어, 애플리케이션 code은 다음과 같이 호출해야 하며, client.orders.listAll() 또는 client.orders.list({ cursor })호출부마다 쿼리 스트링을 직접 조합하거나 페이지네이션 메타데이터를 파싱해서는 안 됩니다.

API이 제품일 때 SDK 스타일 클라이언트가 말이 됩니다

Public API와 공유 플랫폼 서비스에는 생성된 엔드포인트 함수만으로는 부족합니다. 사용자는 일관된 네이밍, 예측 가능한 에러, 그리고 도메인에 맞는 메서드 뒤로 숨겨진 전송 세부 사항을 기대합니다.

좋은 클라이언트 사용성은 보통 다음과 같습니다:

  • client.orders.list() 타입이 지정된 페이지 또는 async iterator 반환
  • client.files.stream() 저수준 fetch 설정이 호출부로 노출되지 않도록 스트리밍 처리
  • auth는 전역으로 설정하고 요청별로 오버라이드할 수 있습니다
  • 호출자는 그때그때 throw되는 페이로드 대신 안정적인 타입 지정 에러 객체를 받습니다

유지보수 비용이 늘어납니다. 또한 각 팀이 동일한 경계 규칙을 조금씩 다르게 다시 만드는 일을 막아주는데, 계약 드리프트는 바로 이렇게 퍼집니다.

완전한 타입 지정된 클라이언트는 결승선이 아닙니다. API가 진화해도 타입이 여전히 현실과 일치하는 클라이언트가 진짜 결승선입니다. 생성이 공개된 계약(public contract)에서 시작되고, 런타임 검증이 경계를 보호하며, DTO 매핑이 내부 변경이 외부로 새어나가는 것을 막기 때문입니다.

실제로 도움이 되는 에러 핸들링·테스트·Observability

대부분의 TypeScript API 예제는 지나치게 평온합니다. 요청은 성공하고, JSON은 인터페이스와 일치하며, 실패는 throw new Error("something went wrong")가 됩니다. 프로덕션은 그렇게 예의 바르게 행동하지 않습니다.

첫 번째 수정은 기계적인 것입니다. TypeScript에서 catch된 값은 unknown로 취급한 뒤, 읽기 전에 좁혀야 합니다 message, stack, 또는 응답 속성. 전문가 가이드도 커스텀 에러 클래스를 권장하며, cause로 원래 실패를 보존하고, 경계에서 검증하며, Error가 아닌 throw를 정규화하고, 관찰 가능성(observability)을 위해 요청 컨텍스트를 첨부하는 것을 권장합니다(TypeScript 에러 핸들링 가이드).

TypeScript 환경에서 안정적인 프로덕션 code를 작성하기 위한 5가지 베스트 프랙티스를 담은 인포그래픽입니다.

에러를 다루기 전에 범위를 좁히세요

안전하지 않은 catch 블록은 여전히 흔합니다:

try {
  await client.orders.create(input)
} catch (error) {
  logger.error(error.message)
}

그건 너무 많은 걸 가정합니다. error 아닐 수도 있고 Error 전혀 아닐 수도 있습니다.

더 안전한 패턴:

try {
  await client.orders.create(input)
} catch (error: unknown) {
  if (error instanceof Error) {
    logger.error({ message: error.message, stack: error.stack })
    throw new OrderSyncError("Order sync failed", { cause: error })
  }

  logger.error({ error })
  throw new OrderSyncError("Order sync failed", { cause: new Error("Non-Error thrown") })
}

조금 더 무거워 보이지만, 서드파티 SDK 오류, JSON 파싱 실패, 예상치 못한 throw가 발생할 때 훨씬 더 잘 견딥니다.

일시적인 에러일 때만 재시도하세요

두 번째 큰 개선은 에러 분류입니다. TypeScript SDK 및 API 작업 가이드가 공통으로 내세우는 명확한 규칙은 다음과 같습니다: 네트워크 에러나 HTTP 429, 503 응답 같은 일시적 실패는 재시도하고, 조기에 검증하고, 에러 컨텍스트를 보존하며, 비즈니스 규칙 실패에는 재시도하지 마세요.. 동일한 가이드라인은 또한 Promise.all 빠른 실패(fail-fast) 병렬 작업에 Promise.allSettled 부분 성공이 허용되는 경우 (SDK 에러 처리 패턴).

세 가지 범주로 나눕니다:

  • Validation 에러 프로세스를 떠나기 전에 요청이 잘못된 경우를 의미합니다.
  • Transient 에러 백오프를 적용한 재시도 시 성공할 수 있습니다.
  • Permanent 에러 비즈니스 규칙, 권한, 또는 리소스 누락을 반영하며 직접 노출되어야 합니다.

이 분류는 범용적인 “실패 시 재시도” 헬퍼보다 더 나은 code를 이끌어냅니다.

필드 규칙: 재시도는 전송 불확실성에 속하며, 도메인 불일치에는 속하지 않습니다.

Observability는 실패를 기록만 하지 말고 설명해야 합니다

컨텍스트가 없는 로그는 Observability가 아닙니다. TypeScript에서 API를 사용할 때, 요청이 경계를 넘는 모든 지점에 correlation ID, route 이름, request metadata, 정규화된 error shape를 첨부하세요.

유용한 기준선:

  • Correlation ID 하나의 inbound 요청을 downstream 호출과 연결
  • 구조화된 로그 prose blob이 아닌 필드를 저장
  • Boundary logs parse 실패를 business exception과 별도로 캡처
  • Alerting status code 볼륨이 아닌 error class와 route를 기준으로 설정

모바일 또는 클라이언트 애플리케이션이 이러한 API를 소비한다면, 업데이트 observability도 중요합니다. 릴레이어에서 실용적인 옵션 중 하나는 Capgo, Capacitor 및 Electron 환경에서 라이브 업데이트를 배포하고 추적하기 위한 타입화된 API를 제공합니다. 클라이언트 측 계약 수정 시 앱 스토어 대기 대신 제어된 롤아웃과 버전별 가시성이 필요할 때 유용합니다. 전체 피드백 루프를 강화하려는 팀을 위해 이 가이드는 앱 관측성 서버 측 로깅과 함께 잘 활용됩니다.

구현만 테스트하지 말고 컨트랙트를 테스트하세요

단위 테스트만으로는 드리프트를 잡아낼 수 없습니다. 드리프트가 발생하는 지점에 테스트를 추가하세요.

  • 경계 검증 테스트: 잘못된 입력을 스키마에 전달하고 실패 형태를 검증하세요.
  • 계약 테스트: 실제 HTTP 응답이 공개된 계약과 일치하는지 확인하세요.
  • 타입화된 에러 검증: 일시적 및 영구적 실패가 올바르게 정규화되는지 확인하세요.
  • 클라이언트 통합 테스트: 생성되거나 래핑된 클라이언트가 실제 응답을 올바르게 파싱하는지 확인하세요.

타입이 지정된 API에 대한 강력한 테스트 스위트는 단순히 code 경로를 증명하는 데 그치지 않습니다. 계약이 여전히 사실과 일치하는지 증명합니다.

확신과 제어력을 갖춘 프로덕션 배포

릴리스 품질은 반복 가능한 루프에서 나옵니다. 영웅적 노력에서 나오는 것이 아닙니다.

TypeScript 파이프라인에서 신뢰할 수 있는 API에는 몇 가지 필수 요소가 있습니다. CI에서의 스키마 검사, 생성된 산출물에 대한 타입 검사, 병합 전 계약 차이 검토, 그리고 클라이언트 환경이 준비되지 않았을 때 속도를 늦추거나 롤백할 수 있는 배포 경로입니다.

버텨내는 릴리스 루프

팀이 따를 수 있도록 프로덕션 체크리스트를 간결하게 유지하는 것을 선호합니다:

  • 계약 불일치 시 CI 실패 처리: OpenAPI가 변경되면 생성된 타입과 클라이언트는 동일한 변경 사항에서 업데이트되어야 합니다.
  • 공유 계약을 의도적으로 버전 관리: 공개 DTO 패키지는 캐주얼한 리팩토링이 아닌 릴리스 규율이 필요합니다.
  • 채널 또는 코호트별로 롤아웃: 모든 사용자에게 한꺼번에 깨지는 통합 변경사항을 노출하지 마세요.
  • 롤백을 단순하게 유지하세요: 컨트랙트, 클라이언트 또는 웹 번들을 되돌리는 작업은 운영적으로 지루해야 합니다.

인프라와 배포 워크플로를 동시에 전환하는 팀을 위해, 이 개발자를 위한 클라우드 마이그레이션 가이드는 유용한 계획 참고자료입니다. API의 신뢰성은 code 변경 시에만 아니라 플랫폼 전환 기간에도 종종 저하되므로요.

정확성만큼 제어도 중요합니다

마지막 프로덕션 습관은 버전별 가시성입니다. 어떤 클라이언트 빌드가 어떤 컨트랙트를 호출하는지, 어떤 릴리스가 성공적으로 채택되었는지, 롤아웃 후 실패가 어디에 집중되는지 알아야 합니다. 이는 즉시 업데이트되지 않는 모바일 및 엣지 분산 사용자에게 특히 중요합니다.

스택에 Capacitor 또는 Electron이 포함되어 있다면, live update 도구는 컨트랙트 버그 수정부터 사용자가 수정 사항을 받기까지의 지연을 줄일 수 있습니다. 중요한 것은 추상적인 '더 빠른 업데이트'가 아닙니다. 핵심은 채널 기반 롤아웃, 롤백 보호, 버전 수준 관찰 가능성 을 통해 컨트랙트 수정 사항을 통제된 상태로 유지하는 것입니다.

스키마, 런타임 검증, 클라이언트 생성, 릴리스 운영이 모두 서로를 강화할 때 타입드 API는 건강하게 유지됩니다. 한 레이어를 놓치면 다른 레이어들이 나쁘게 보상하게 됩니다.


Capgo는 Capacitor 및 Electron 앱을 출시하는 팀이 웹 번들 수정 사항을 타입 안정적으로 배포하고, 롤아웃 채널을 관리하며, 버전별 채택률과 실패를 모니터링할 수 있는 방법을 제공합니다. API 계약 수정 사항도 스토어 리뷰를 기다리지 않고 빠르게 클라이언트에 전달해야 한다면, 다음을 방문하세요. Capgo.

Capacitor 앱용 Live updates

웹 레이어 버그가 라이브 상태라면, 앱 스토어 승인을 며칠씩 기다리지 말고 Capgo를 통해 수정 사항을 배포하세요. 네이티브 변경 사항은 정상 리뷰 절차를 그대로 따르는 반면, 사용자는 백그라운드에서 업데이트를 받습니다.

블로그 최신 글

Capgo는 정말 프로페셔널한 모바일 앱을 만드는 데 필요한 최고의 인사이트를 제공합니다.