지난 시간에는 제네릭의 깊은 도구들을 다뤘습니다. 이번이 시리즈의 마지막 글입니다. 두 가지를 정리하며 마무리합니다 — 실전에서 매일 쓰게 될 표준 유틸리티 타입, 그리고 컴파일 동작을 결정하는 **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> — 모든 프로퍼티를 옵셔널로
type User = { id: string; name: string; age: number };
type UserPatch = Partial<User>;
// = { id?: string; name?: string; age?: number }업데이트 함수의 인자 타입에 자주 쓰입니다.
function updateUser(id: string, patch: Partial<User>): void {
// id로 사용자 찾고 patch의 필드만 덮어쓰기
}
updateUser('u-1', { name: '영희' }); // ✓
updateUser('u-1', { name: '영희', age: 28 }); // ✓Required<T> — 모든 옵셔널을 필수로
Partial의 반대. ?로 표시된 프로퍼티들이 필수가 됩니다.
type Config = { host?: string; port?: number; timeout?: number };
type CompleteConfig = Required<Config>;
// = { host: string; port: number; timeout: number }Readonly<T> — 모든 프로퍼티를 readonly로
type ImmutableUser = Readonly<User>;
const u: ImmutableUser = { id: 'u-1', name: '철수', age: 30 };
u.name = '영희'; // 🚫키 선택 — Pick / Omit / Record
Pick<T, K> — 일부 키만 골라내기
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> — 일부 키 제외
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> — 키-값 매핑 객체
type Scores = Record<string, number>;
// = { [key: string]: number }
const scores: Scores = {
math: 95,
english: 87,
science: 91,
};특정 키 union으로 제한할 수도 있어요.
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에서 일부 타입 제거
type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type WarmColors = Exclude<AllColors, 'green' | 'blue'>;
// = 'red' | 'yellow'T의 union에서 U에 해당하는 타입을 빼냅니다.
Extract<T, U> — union에서 일부 타입만 추출
type Mixed = string | number | boolean | null | undefined;
type Truthy = Extract<Mixed, string | number | boolean>;
// = string | number | booleanExclude와 거울상.
NonNullable<T> — null/undefined 제거
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// = UserT | null | undefined를 자주 다루다 보면 매우 자주 쓰게 되는 유틸이에요.
함수 관련 — ReturnType / Parameters / Awaited
ReturnType<F> — 함수의 반환 타입
function fetchUser() {
return { id: 'u-1', name: '철수' };
}
type User = ReturnType<typeof fetchUser>;
// = { id: string; name: string }#6에서 잠깐 본 패턴입니다. 함수가 반환하는 객체의 모양을 별도 타입으로 정의 안 하고 자동으로 가져옵니다.
Parameters<F> — 함수의 매개변수 타입들 (튜플로)
function login(username: string, password: string): boolean {
return true;
}
type LoginArgs = Parameters<typeof login>;
// = [username: string, password: string]Awaited<T> — Promise를 풀어낸 타입
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
{
"compilerOptions": {
"strict": true
}
}strict: true는 여러 안전성 옵션을 한 번에 켜는 마스터 스위치입니다.
noImplicitAny— 추론 실패 시 암묵적 any를 금지strictNullChecks—null과undefined를 다른 타입과 명확히 구분strictFunctionTypes— 함수 매개변수 타입 호환성 엄격하게strictBindCallApply—bind/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 프로젝트의 경우
{ "compilerOptions": { "jsx": "react-jsx" } }JSX를 어떻게 변환할지 결정. 모던 React라면 react-jsx, Next.js라면 preserve(Next.js가 직접 처리).
종합 — 모던 권장 설정
새 프로젝트라면 다음 정도가 기본 좋은 출발점입니다.
{
"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 모듈 호환성 향상. 거의 항상 trueskipLibCheck— 외부 라이브러리의.d.ts검사를 건너뜀. 컴파일 속도 ↑. 거의 항상 truedeclaration—.d.ts파일도 함께 생성. 라이브러리를 만들 때 필요noEmit— true면 컴파일 결과를 출력하지 않음. 타입 검사 전용으로 사용 (Vite 같은 다른 도구가 트랜스파일하는 경우)
Vite로 만든 프로젝트는 tsc가 트랜스파일을 안 하고 타입 검사만 하므로 noEmit: true로 설정되어 있을 겁니다.
시리즈 회고
이 시리즈에서 우리는 다음을 다뤘습니다.
| # | 주제 | 핵심 |
|---|---|---|
| 1 | 시작과 셋업 | TS motivation, 컴파일 흐름, 첫 코드 |
| 2 | 기본 타입 | string/number/boolean/array/tuple/object/enum, any/unknown |
| 3 | interface와 type alias | 객체 타입 별명, 두 도구의 차이 |
| 4 | union/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로 시작해보세요. 막히는 곳에서 진짜 학습이 일어납니다. 다음 시리즈에서 또 만나요!