타입스크립트 기초 강좌 #7 유틸리티 타입과 tsconfig

21 분 소요

지난 시간에는 제네릭의 깊은 도구들을 다뤘습니다. 이번이 시리즈의 마지막 글입니다. 두 가지를 정리하며 마무리합니다 — 실전에서 매일 쓰게 될 표준 유틸리티 타입, 그리고 컴파일 동작을 결정하는 **tsconfig.json**의 핵심 옵션들.

유틸리티 타입이란

타입스크립트에는 자주 쓰는 타입 변환 패턴이 빌트인 유틸리티 타입으로 미리 정의돼 있습니다. import 없이 어디서나 바로 쓸 수 있어요.

유틸리티 타입 예시
type User = { id: string; name: string; age: number; email: string };
 
type UserPreview = Pick<User, 'id' | 'name'>;
//   = { id: string; name: string }
 
type UserUpdate = Partial<User>;
//   = { id?: string; name?: string; age?: number; email?: string }

이 변환들은 #6에서 본 매핑된 타입과 인덱스 액세스 같은 도구로 만들어져 있습니다 — 타입스크립트 자체로 표현 가능한 거예요. 우리도 직접 만들 수 있지만, 자주 쓰는 건 표준 라이브러리에 있으니 그걸 쓰면 됩니다.

대표적인 것들을 카테고리별로 살펴봅시다.

객체 변형 — Partial / Required / Readonly

Partial<T> — 모든 프로퍼티를 옵셔널로

Partial
type User = { id: string; name: string; age: number };
type UserPatch = Partial<User>;
// = { id?: string; name?: string; age?: number }

업데이트 함수의 인자 타입에 자주 쓰입니다.

patch 함수
function updateUser(id: string, patch: Partial<User>): void {
  // id로 사용자 찾고 patch의 필드만 덮어쓰기
}
 
updateUser('u-1', { name: '영희' });        // ✓
updateUser('u-1', { name: '영희', age: 28 }); // ✓

Required<T> — 모든 옵셔널을 필수로

Partial의 반대. ?로 표시된 프로퍼티들이 필수가 됩니다.

Required
type Config = { host?: string; port?: number; timeout?: number };
type CompleteConfig = Required<Config>;
// = { host: string; port: number; timeout: number }

Readonly<T> — 모든 프로퍼티를 readonly로

Readonly
type ImmutableUser = Readonly<User>;
 
const u: ImmutableUser = { id: 'u-1', name: '철수', age: 30 };
u.name = '영희';   // 🚫

키 선택 — Pick / Omit / Record

Pick<T, K> — 일부 키만 골라내기

Pick
type User = { id: string; name: string; age: number; email: string };
type UserPreview = Pick<User, 'id' | 'name'>;
// = { id: string; name: string }

K는 T의 키 union입니다. 거기서 골라낸 프로퍼티들로만 새 타입을 만들어요.

Omit<T, K> — 일부 키 제외

Omit
type UserWithoutEmail = Omit<User, 'email'>;
// = { id: string; name: string; age: number }
 
type UserWithoutEmailAndAge = Omit<User, 'email' | 'age'>;
// = { id: string; name: string }

여러 키를 제외할 때 union으로 묶어 넘깁니다. Pick과 Omit은 거의 거울상이에요. "필요한 것 골라내기"는 Pick, "필요 없는 것 빼기"는 Omit.

Record<K, V> — 키-값 매핑 객체

Record
type Scores = Record<string, number>;
// = { [key: string]: number }
 
const scores: Scores = {
  math: 95,
  english: 87,
  science: 91,
};

특정 키 union으로 제한할 수도 있어요.

Record with literal keys
type Roles = 'admin' | 'editor' | 'viewer';
type Permissions = Record<Roles, string[]>;
 
const perms: Permissions = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read'],
};
// 셋 중 하나라도 빠뜨리면 컴파일 에러

이 패턴은 enum-like 데이터에 매핑되는 정보를 안전하게 표현할 때 강력해요.

Union 변형 — Exclude / Extract / NonNullable

Exclude<T, U> — union에서 일부 타입 제거

Exclude
type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type WarmColors = Exclude<AllColors, 'green' | 'blue'>;
// = 'red' | 'yellow'

T의 union에서 U에 해당하는 타입을 빼냅니다.

Extract<T, U> — union에서 일부 타입만 추출

Extract
type Mixed = string | number | boolean | null | undefined;
type Truthy = Extract<Mixed, string | number | boolean>;
// = string | number | boolean

Exclude와 거울상.

NonNullable<T> — null/undefined 제거

NonNullable
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// = User

T | null | undefined를 자주 다루다 보면 매우 자주 쓰게 되는 유틸이에요.

함수 관련 — ReturnType / Parameters / Awaited

ReturnType<F> — 함수의 반환 타입

ReturnType
function fetchUser() {
  return { id: 'u-1', name: '철수' };
}
 
type User = ReturnType<typeof fetchUser>;
// = { id: string; name: string }

#6에서 잠깐 본 패턴입니다. 함수가 반환하는 객체의 모양을 별도 타입으로 정의 안 하고 자동으로 가져옵니다.

Parameters<F> — 함수의 매개변수 타입들 (튜플로)

Parameters
function login(username: string, password: string): boolean {
  return true;
}
 
type LoginArgs = Parameters<typeof login>;
// = [username: string, password: string]

Awaited<T> — Promise를 풀어낸 타입

Awaited
type UserPromise = Promise<User>;
type UnwrappedUser = Awaited<UserPromise>;
// = User
 
// 중첩된 Promise도 풀어줌
type Nested = Promise<Promise<User>>;
type Unwrapped = Awaited<Nested>;
// = User (중첩까지 풀어냄)

async 함수의 반환 타입 처리에 자주 쓰여요.

종합 예시 — 실전 같은 패턴

지금까지 본 유틸리티들이 한 곳에 모이는 작은 예시.

실전 패턴
type User = {
  id: string;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
};
 
// 클라이언트에 노출 — password 제거
type PublicUser = Omit<User, 'password'>;
 
// 가입 폼에서 받는 데이터 — id와 createdAt은 서버가 결정
type SignupInput = Omit<User, 'id' | 'createdAt'>;
 
// 프로필 업데이트 — id 빼고 모두 옵셔널
type ProfileUpdate = Partial<Omit<User, 'id'>>;
 
// 사용자 목록 카드 — 일부만 필요
type UserCard = Pick<User, 'id' | 'name'>;
 
// 권한 매핑
type Role = 'admin' | 'editor' | 'viewer';
type RolePermissions = Record<Role, string[]>;
 
// API 함수 시그니처에서 매개변수 추출
async function getUser(id: string): Promise<PublicUser> {
  // ...
  return {} as PublicUser;
}
 
type GetUserArgs = Parameters<typeof getUser>;     // [id: string]
type GetUserResult = Awaited<ReturnType<typeof getUser>>;  // PublicUser

같은 User 데이터 모양에서 상황별로 필요한 변형을 모두 유도해냅니다. 데이터 모양은 한 곳에 정의하고, 거기서 다양한 뷰를 파생시키는 거죠. 데이터-타입 동기화 부담이 사라지고, User 정의가 바뀌면 파생 타입들이 자동으로 따라 갱신됩니다.

tsconfig.json — 컴파일러 설정

이제 두 번째 주제로 넘어갑니다. tsconfig.json은 타입스크립트 컴파일러의 모든 동작을 결정하는 설정 파일이에요.

npx tsc --init으로 만들어지는 기본 설정에는 자세한 주석이 달려 있습니다. 한 번 열어보면 옵션이 100개가 넘게 보일 거예요. 다행히 일상적으로 신경 쓰는 건 그중 십 수 개뿐입니다.

핵심 옵션 — strict

tsconfig.json (일부)
{
  "compilerOptions": {
    "strict": true
  }
}

strict: true는 여러 안전성 옵션을 한 번에 켜는 마스터 스위치입니다.

  • noImplicitAny — 추론 실패 시 암묵적 any를 금지
  • strictNullChecksnullundefined를 다른 타입과 명확히 구분
  • strictFunctionTypes — 함수 매개변수 타입 호환성 엄격하게
  • strictBindCallApplybind/call/apply 인자 검사
  • strictPropertyInitialization — 클래스 프로퍼티 초기화 강제
  • noImplicitThis — 암묵적 any인 this 금지

새 프로젝트라면 항상 strict: true로 시작하세요. 이 옵션을 끄면 타입스크립트의 가장 큰 가치가 절반 이상 사라집니다. 기존 자바스크립트 프로젝트를 마이그레이션하는 경우라면 점진적으로 켜기도 하지만, 새 프로젝트에서 strict 끄는 건 거의 항상 잘못된 선택이에요.

target — 어떤 자바스크립트로 변환할지

{ "compilerOptions": { "target": "ES2022" } }

타입스크립트가 어떤 버전의 자바스크립트로 변환할지 결정합니다.

  • ES5 — 옛 IE까지 지원해야 할 때 (드물어짐)
  • ES2017~ES2022 — 모던 브라우저
  • ESNext — 최신

브라우저 대상이면 보통 ES2020 이상, Node.js 최신 버전 대상이면 ES2022 정도가 무난합니다.

module — 모듈 시스템

{ "compilerOptions": { "module": "ESNext" } }

import/export 코드를 어떻게 변환할지 결정.

  • CommonJS — Node.js 전통 방식 (require)
  • ESNext / ES2022 — 표준 ES 모듈
  • NodeNext — Node.js의 ES 모듈 + CommonJS 혼용 처리

번들러(Vite, Webpack)를 쓴다면 거의 항상 ESNext. 순수 Node.js 프로젝트면 NodeNext가 보통 답입니다.

moduleResolution — 모듈 찾기 방식

{ "compilerOptions": { "moduleResolution": "bundler" } }

타입스크립트가 import 경로를 해석하는 방식. 최근에는 bundler(Vite/Webpack 등 번들러를 위한 새 모드)나 NodeNext가 권장됩니다.

lib — 사용 가능한 빌트인 API

{ "compilerOptions": { "lib": ["ES2022", "DOM", "DOM.Iterable"] } }

타입스크립트가 인식할 빌트인 라이브러리 타입들. 브라우저에서 동작한다면 DOM을 포함하고, Node.js 전용이라면 빼고 @types/node를 사용.

outDir / rootDir — 입출력 경로

간단한 설정
{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist"
  }
}
  • rootDir — 소스 코드의 루트 (어디부터 컴파일 시작할지)
  • outDir — 출력 파일들의 위치

outDir을 지정하면 컴파일된 .js가 그 폴더에 모입니다. 지정 안 하면 .ts 파일과 같은 위치에 생성돼서 폴더가 지저분해져요.

include / exclude — 어떤 파일을 컴파일할지

대상 파일 지정
{
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

include로 컴파일 대상을 지정하고 exclude로 제외할 폴더를 나열. node_modules는 거의 항상 exclude에 포함되어 있어요.

sourceMap — 디버깅용 소스 맵

{ "compilerOptions": { "sourceMap": true } }

.js.map 파일을 추가로 생성해 브라우저/Node 디버거에서 원본 .ts 파일과 매핑이 되게 합니다. 디버깅이 훨씬 편해져요.

jsx — React 프로젝트의 경우

React 프로젝트
{ "compilerOptions": { "jsx": "react-jsx" } }

JSX를 어떻게 변환할지 결정. 모던 React라면 react-jsx, Next.js라면 preserve(Next.js가 직접 처리).

종합 — 모던 권장 설정

새 프로젝트라면 다음 정도가 기본 좋은 출발점입니다.

tsconfig.json (모던 권장)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
 
    "strict": true,
    "noUncheckedIndexedAccess": true,
 
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
 
    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true,
    "declaration": true,
    "noEmit": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

추가로 알아둘 만한 옵션들:

  • noUncheckedIndexedAccess — 배열 인덱스 접근 결과에 | undefined를 추가. arr[0]T가 아니라 T | undefined가 됨. 안전성 매우 ↑, 가끔 귀찮음. 여유 있다면 켜는 걸 권장
  • esModuleInterop — CommonJS와 ES 모듈 호환성 향상. 거의 항상 true
  • skipLibCheck — 외부 라이브러리의 .d.ts 검사를 건너뜀. 컴파일 속도 ↑. 거의 항상 true
  • declaration.d.ts 파일도 함께 생성. 라이브러리를 만들 때 필요
  • noEmit — true면 컴파일 결과를 출력하지 않음. 타입 검사 전용으로 사용 (Vite 같은 다른 도구가 트랜스파일하는 경우)

Vite로 만든 프로젝트는 tsc가 트랜스파일을 안 하고 타입 검사만 하므로 noEmit: true로 설정되어 있을 겁니다.

시리즈 회고

이 시리즈에서 우리는 다음을 다뤘습니다.

#주제핵심
1시작과 셋업TS motivation, 컴파일 흐름, 첫 코드
2기본 타입string/number/boolean/array/tuple/object/enum, any/unknown
3interface와 type alias객체 타입 별명, 두 도구의 차이
4union/literal/narrowing여러 가능성 표현 + 타입 좁히기
5함수 타입옵셔널/디폴트/rest, 오버로드, 제네릭 입문
6제네릭 깊이제약, keyof, 인덱스 액세스, conditional types
7유틸리티 타입 + tsconfig표준 유틸리티, 컴파일 설정

타입스크립트 학습은 한 번에 다 흡수하는 게 아니라 쓰면서 점점 익숙해지는 영역입니다. 이 시리즈로 거의 모든 도구의 이름과 모양을 만났고, 이제는 코드를 쓰다가 막히는 곳에서 "아, #4에서 봤던 그 narrowing 패턴이 필요하겠네" 하고 떠올릴 수 있게 됐을 거예요.

다음 단계

즉시 도전할 만한 것

  • 본인의 작은 자바스크립트 프로젝트를 TS로 마이그레이션.js.ts로 이름 바꾸고 에러를 하나씩 잡기
  • 새 프로젝트는 처음부터 TS로 — Vite든 Next.js든 TS 옵션 선택
  • 라이브러리의 .d.ts 파일 한 번 읽어보기node_modules/lodash/index.d.ts 같은 곳을 열어보면 실전 타입의 모범 사례가 보임

곧 필요해질 영역

  • @types/... 패키지 — 타입 없는 라이브러리에 타입 추가
  • Zod 또는 Valibot — 런타임 검증 라이브러리. TS와 결합해 안전성 ↑
  • TypeScript ESLint — 린트 규칙 (예: no-explicit-any, prefer-const)

후속 시리즈 — React + TypeScript

방금 끝낸 리액트 31편이 모두 자바스크립트로 작성됐는데, 다음으로 React + TypeScript 시리즈를 통해 그 모든 패턴을 TS 안에서 어떻게 표현하는지 다룰 예정입니다. props 타이핑, 훅 타이핑, 제네릭 컴포넌트, 폴리모피즘 — 실무에서 가장 자주 쓰는 패턴들이에요.

마무리

여기까지 7편 시리즈를 따라와주셔서 감사합니다. 타입스크립트는 처음 만나면 가파른 학습 곡선이 있지만, 일정 임계점을 넘으면 자바스크립트로 다시 못 돌아갈 정도로 편안해지는 도구예요. 자동완성 / 리팩터링 안전성 / 즉각적인 피드백의 가치는 한 번 경험하면 잊기 어렵습니다.

기억에 남길 한 가지가 있다면 — **"본인의 데이터 모양을 명시하는 습관"**입니다. 함수 시그니처, 객체 타입, API 응답 모양을 코드 위쪽에 명시적으로 적는 습관 자체가 좋은 코드를 쓰게 만들어요. 타입은 결국 코드에 대해 더 깊이 생각하게 만드는 도구예요.

직접 만들고 싶은 작은 프로젝트로 돌아가, 이번에는 TypeScript로 시작해보세요. 막히는 곳에서 진짜 학습이 일어납니다. 다음 시리즈에서 또 만나요!