타입스크립트는 단순히 "타입을 붙인 자바스크립트"가 아닙니다. 제대로 활용하면 버그를 실행 전에 잡아내는 정적 분석기이자, 코드의 문서이자, 팀원 간 커뮤니케이션의 계약이 됩니다. 하지만 이 언어의 타입 시스템은 깊이가 상당해서, 많은 팀이 any와 단순 인터페이스 정도로만 사용하고 있습니다. Claude Code는 이 심화 영역에 진입하는 데 완벽한 동반자입니다. 복잡한 제네릭, 조건부 타입, 매핑 타입을 사람이 작성하려면 많은 시행착오가 필요하지만, Claude는 요구사항만 명확히 전달하면 거의 즉시 정확한 타입을 제안합니다. 이 글은 Claude와 함께 타입스크립트의 타입 안전성을 최대로 끌어내는 실전 기법을 정리합니다.

코드 에디터에 표시된 TypeScript 코드

1. 왜 타입 안전성이 중요한가

타입 시스템의 가치는 "버그를 잡는 것"에 있지 않습니다. 진짜 가치는 "리팩토링을 두려워하지 않게 만드는 것"에 있습니다. 잘 설계된 타입은 잘못된 변경을 IDE가 즉시 붉은 밑줄로 알려 주며, 이는 팀이 코드를 자주 개선할 수 있는 심리적 안전망을 제공합니다. Claude Code에 "이 함수의 시그니처를 바꾸면 어디가 깨지는가"를 물으면, 타입 체커 없이도 논리적인 영향 범위를 얻을 수 있지만, 타입 시스템이 튼튼하면 그 답이 훨씬 신뢰할 만합니다.

2. tsconfig 최적화

모든 것은 엄격한 tsconfig.json에서 시작됩니다. Claude에 "이 프로젝트의 tsconfig를 엄격 모드로 업그레이드하고, 발생하는 에러를 모두 수정해 줘"라고 요청하세요. 권장 기본값은 다음과 같습니다.

{
  "compilerOptions": {
    "strict": true,
    "noImplicitOverride": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noFallthroughCasesInSwitch": true,
    "useUnknownInCatchVariables": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

특히 noUncheckedIndexedAccess는 배열/객체 인덱싱 결과를 반드시 undefined 체크하게 만들어, 실무에서 가장 흔한 런타임 에러 부류를 원천 차단합니다.

3. 브랜드 타입으로 기본 타입 구분

"string인데 UserId일 수도 있고 Email일 수도 있는" 상태는 지연된 버그의 온상입니다. 브랜드 타입으로 구분하면 함수 시그니처 수준에서 혼동을 방지할 수 있습니다. Claude에 "UserId, OrderId, Email을 브랜드 타입으로 정의하고, 파싱 함수를 작성해 줘"라고 요청하면, Zod나 수동 체크 기반의 깔끔한 구현을 얻을 수 있습니다.

type Brand<T, B> = T & { readonly __brand: B };
export type UserId = Brand<string, 'UserId'>;
export type Email = Brand<string, 'Email'>;

export const asUserId = (raw: string): UserId => {
  if (!/^[0-9a-f-]{36}$/.test(raw)) throw new Error('invalid user id');
  return raw as UserId;
};

4. 판별 유니언으로 상태 모델링

상태를 "isLoading, error, data" 같은 독립 필드로 표현하면 불가능한 조합이 생깁니다. 판별 유니언으로 재모델링하면 "로딩 중에는 data가 없다"가 타입 수준에서 강제됩니다. Claude에 기존 코드를 넘기고 "이 상태를 판별 유니언으로 재설계해 줘"라고 요청하면, 모범적인 리팩토링을 얻습니다.

type Query<T, E> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: E };

5. 제네릭의 올바른 사용

제네릭은 "타입 파라미터를 끼워 넣는 것" 이상입니다. 올바른 제약조건을 붙여야 강력해집니다. Claude에 "이 함수에 적절한 제약이 있는 제네릭을 적용해 줘"라고 요청하되, 제약은 최소한으로 유지해 달라고 덧붙이세요. 과한 제약은 호출부의 유연성을 해칩니다.

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const out = {} as Pick<T, K>;
  for (const k of keys) out[k] = obj[k];
  return out;
}

6. 조건부 타입과 infer

조건부 타입은 타입스크립트의 "if문"입니다. infer와 결합하면 타입에서 다른 타입을 추출할 수 있습니다. Claude는 이런 타입 추론을 굉장히 잘 다룹니다. 예를 들어 "함수의 반환 타입을 언래핑해 줘" 같은 요청에도 정확한 Awaited<T> 기반 해법을 제안합니다.

type UnaryArg<F> = F extends (arg: infer A) => unknown ? A : never;
type Result<F> = F extends (...args: any[]) => Promise<infer R> ? R : never;

7. 템플릿 리터럴 타입

템플릿 리터럴 타입은 문자열 수준에서 타입 안전성을 강화합니다. 이벤트 이름, 경로, CSS 변수 이름을 타입으로 표현하면 오타가 컴파일 시점에 잡힙니다. Claude에 "우리 이벤트 이름 규칙을 템플릿 리터럴 타입으로 인코딩해 줘"라고 요청하면, 일관된 네이밍이 타입 수준에서 강제됩니다.

type Event =
  | \`user.\${'created' | 'updated' | 'deleted'}\`
  | \`order.\${'placed' | 'canceled' | 'fulfilled'}\`;

8. 매핑 타입과 유틸리티

매핑 타입은 기존 타입을 변환해 새로운 타입을 만듭니다. Partial, Required, Readonly, Record, Pick, Omit은 기본이며, 이 위에 팀 전용 유틸리티를 조금만 더하면 표현력이 크게 늘어납니다. Claude에 "DeepReadonly, DeepPartial, DeepNonNullable을 작성해 줘"라고 요청하면, 재귀 매핑 타입을 즉시 받을 수 있습니다.

9. 런타임과 컴파일 타임의 일치

타입 안전성의 최대 적은 "외부 세계"입니다. 네트워크 응답, 폼 입력, LocalStorage는 모두 unknown이어야 하며, 이를 안전하게 T로 좁히는 검증이 필요합니다. zod, valibot, @effect/schema 같은 라이브러리를 쓰면 스키마 한 곳에서 런타임 검증과 컴파일 타임 타입을 동시에 얻을 수 있습니다. Claude에 "이 API 응답 타입을 zod 스키마로 변환해 줘"라고 하면 반복 작업이 사라집니다.

import { z } from 'zod';
export const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest'])
});
export type User = z.infer<typeof UserSchema>;

10. 오류 처리: Result 패턴

try/catch는 편하지만 타입 시스템이 에러 경로를 추적하지 않습니다. Result 패턴으로 에러를 값으로 승격시키면, 컴파일러가 에러 처리를 강제합니다. Claude에 "이 함수를 Result<T, E> 패턴으로 변환해 줘"라고 요청해 보세요. 처음에는 장황해 보이지만, 한두 주 쓰다 보면 팀의 에러 처리 품질이 크게 올라갑니다.

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

async function fetchUser(id: UserId): Promise<Result<User, 'not_found' | 'network'>> {
  const res = await fetch(\`/api/users/\${id}\`);
  if (res.status === 404) return { ok: false, error: 'not_found' };
  if (!res.ok) return { ok: false, error: 'network' };
  return { ok: true, value: UserSchema.parse(await res.json()) };
}

11. 함수 오버로드와 타입 좁히기

동일한 함수가 여러 시그니처를 가지는 경우, 오버로드로 정확한 관계를 표현할 수 있습니다. Claude에 "이 함수의 오버로드 시그니처를 최소 집합으로 정리해 줘"라고 요청하면, 호출부에서 불필요한 as 캐스팅이 사라집니다.

12. Exhaustive check

유니언 타입을 처리할 때 한 분기를 빠뜨리면 런타임 버그가 됩니다. 빈틈없는 처리임을 타입 시스템이 보장하도록 never 기반 헬퍼를 사용하세요.

function assertNever(x: never): never {
  throw new Error(\`Unexpected: \${JSON.stringify(x)}\`);
}

switch (event.type) {
  case 'created': return handleCreate(event);
  case 'updated': return handleUpdate(event);
  case 'deleted': return handleDelete(event);
  default: return assertNever(event);
}

13. 테스트에서의 타입 확인

타입도 테스트할 수 있습니다. expect-type이나 tsd를 사용해 "이 함수의 반환 타입이 정확히 X여야 한다"를 보장하세요. 라이브러리 성격의 코드라면 필수입니다. Claude에 "이 타입 유틸리티에 대한 타입 테스트를 작성해 줘"라고 하면 유용한 케이스들을 만들어 줍니다.

14. 타입스크립트 성능 관리

타입 시스템이 고도화될수록 컴파일 속도가 느려질 수 있습니다. 다음 원칙을 따르면 성능을 유지할 수 있습니다.

15. 서드파티 타입과의 공존

서드파티 라이브러리의 타입이 엄격하지 않을 때는, 자체 래퍼로 감싸는 전략이 가장 깔끔합니다. Claude에 "이 라이브러리의 API를 더 안전한 형태로 래핑해 줘"라고 요청하면, 입력 검증과 에러 처리가 포함된 어댑터를 얻을 수 있습니다.

문제권장 해법
any가 광범위하게 퍼짐Zod 스키마 도입, noImplicitAny 강제
유니언 분기 누락exhaustive check 헬퍼
런타임 에러 타입 추적 불가Result 패턴
같은 string 혼동브랜드 타입
객체 일부만 허용하는 계약Pick, Omit, satisfies

팁: satisfies 연산자를 적극 활용하세요. "이 값이 X를 만족하는가"를 검증하면서도, 더 좁은 구체 타입을 유지할 수 있어 상수 선언에 특히 유용합니다.

16. 팀 단위 도입 전략

타입 안전성을 팀에 도입할 때는 급진적 전환보다 점진적 개선이 효과적입니다. 다음 단계를 권장합니다.

  1. strict 모드의 하위 플래그를 하나씩 켜기.
  2. 각 모듈별로 any 개수를 측정, 매주 감소 목표 설정.
  3. 새 코드부터 브랜드 타입, Zod 스키마 적용.
  4. Claude에 "레거시 코드에서 any를 찾아 가장 쉬운 것부터 정리해 줘" 요청.
  5. PR 템플릿에 "새 any가 도입되지 않았는가?" 체크박스 추가.

마무리: 타입은 문서다

타입스크립트를 진지하게 쓰면 쓸수록, 타입이 코드의 의도를 가장 정확히 표현하는 문서라는 사실을 체감하게 됩니다. 주석은 낡지만, 타입은 컴파일러가 매번 검증해 줍니다. Claude Code는 이 문서를 사람보다 빠르게, 그리고 일관된 스타일로 작성해 줄 수 있는 동료입니다.

오늘 공유한 기법을 한꺼번에 도입할 필요는 없습니다. 다음 주에는 tsconfig를 엄격하게, 그다음 주에는 브랜드 타입을 한두 개, 또 그다음 주에는 Zod 스키마를 한 모듈에. 이런 식으로 한 걸음씩 옮기다 보면, 6개월 뒤에는 "배포 후에야 발견하던 버그들"이 대부분 컴파일 시점에 잡히는 프로젝트를 갖게 될 것입니다. Claude Korea는 계속해서 언어별 심화 가이드를 이어갈 예정입니다.

타입은 제약이 아닙니다. 오히려 자유롭게 변경할 수 있는 용기를 주는 안전망입니다.

관련 리소스