타입스크립트 프로그래밍의 베스트 프랙티스

타입스크립트 프로그래밍의 베스트 프랙티스

이 기사에서는 타입스크립트 프로그래밍의 베스트 프랙티스를 설명합니다.

이 가이드에서는 타입스크립트의 타입을 활용하여 버그를 줄이고, 더 읽기 쉬운 코드를 작성하기 위한 실용적인 베스트 프랙티스를 설명합니다.

YouTube Video

타입스크립트 프로그래밍의 베스트 프랙티스

타입스크립트의 가장 큰 장점은 '타입으로 버그를 예방하고 코드의 의도를 명확하게 만드는 것'입니다.

베스트 프랙티스란 단순한 규칙이 아니라, 안전하고 읽기 쉬우며 유지보수가 쉬운 코드를 작성하기 위한 원칙의 집합입니다. 아래에서는 자주 사용되는 타입스크립트 베스트 프랙티스를 실제 예시와 함께 소개합니다.

any 사용을 피하고, 항상 의미 있는 타입 정의를 부여하세요

먼저 'any를 피하고 의미 있는 타입을 부여하는 것'에 대해 살펴봅시다.

any는 타입 체크를 완전히 비활성화하기 때문에, 타입스크립트의 목적을 무의미하게 만듭니다. 그저 동작시키기 위해 any를 사용하는 대신, 되도록 상세하게 타입을 정의하는 것이 중요합니다.

1// Bad
2function parse(data: any) {
3  return data.value;
4}

이 코드는 어떤 값이든 받을 수 있으므로, 런타임 에러를 방지할 수 없습니다.

1// Good
2type ParsedData = {
3  value: string;
4};
5
6function parse(data: ParsedData): string {
7  return data.value;
8}

타입을 정의함으로써 입력 및 출력의 의도를 명확하게 하고, 안전성을 높일 수 있습니다.

항상 type 또는 interface를 사용하여 객체 구조를 명확하게 정의하세요.

다음으로 'type이나 interface를 사용하여 객체 구조를 항상 명확하게 정의하는 것'에 대해 살펴봅시다.

객체를 임시로 사용하면 구조가 불명확해질 수 있습니다. 재사용성과 유지보수성을 위해 항상 타입을 분리하세요.

1// Bad
2function createUser(user: { name: string; age: number }) {
3  console.log(user.name);
4}

작은 코드 조각에서도 타입을 분리하는 습관을 기르는 것이 중요합니다.

1// Good
2type User = {
3  name: string;
4  age: number;
5};
6
7function createUser(user: User): void {
8  console.log(user.name);
9}

타입에 이름을 붙이면 전체 코드베이스를 이해하기가 훨씬 쉬워집니다.

모든 가능한 상태를 정확하게 표현하기 위해 유니온 타입을 사용하세요.

조건 분기에서 단순히 string이나 number 타입만 사용하면 예기치 않은 값이 들어갈 수 있습니다. 유니언 타입을 사용하면 타입 수준에서 허용된 상태만 표현할 수 있습니다.

1// Bad
2function setStatus(status: string) {
3  console.log(status);
4}

이 코드에서는 의미 없는 문자열이나 잘못된 값을 컴파일 시점에 감지할 수 없습니다.

1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4function setStatus(status: Status): void {
5  console.log(status);
6}

유니언 타입을 사용하면 컴파일 시점에서 '불가능한 상태'를 확실하게 제거할 수 있습니다. 그 결과, 조건 분기문의 안전성과 코드의 신뢰성이 향상됩니다.

nullundefined는 명확하게 처리하세요.

다음으로 'nullundefined를 명확하게 처리하는 것'에 대해 살펴봅시다.

타입스크립트에서는 값의 부재 가능성을 타입으로 명시하는 것이 중요합니다. 모호하게 방치하면 런타임 에러로 이어질 수 있습니다.

1type User = {
2  name: string;
3  email?: string;
4};

email이 존재하지 않을 수 있으므로, 그 점을 고려하여 처리해야 합니다.

1function printEmail(user: User): void {
2  if (user.email) {
3    console.log(user.email);
4  }
5}

옵셔널 값은 사용 전에 항상 체크하세요.

타입 단언(as)은 최후의 수단으로만 사용하세요.

다음으로 '타입 단언을 과도하게 사용하지 않는 것'에 대해 살펴봅시다.

타입 단언은 일시적으로 TypeScript의 타입 검사를 우회하여 '이 값이 이 타입임을 알고 있다'고 선언합니다. 과도하게 사용하면 타입 안정성을 해칩니다.

1// Bad
2const value = input as string;

이 코드에서는 실제 값이 문자열이 아니더라도 오류가 발생하지 않아 런타임 오류의 원인이 될 수 있습니다. 다음 코드와 같이 타입 가드를 사용하여 안전하게 먼저 확인하는 것을 선택하세요.

1// Good
2function isString(value: unknown): value is string {
3  return typeof value === "string";
4}
5
6if (isString(input)) {
7  console.log(input.toUpperCase());
8}

타입 가드는 실제 값을 확인하면서 타입을 안전하게 판별하는 메커니즘입니다. 타입 단언보다 타입 가드를 우선적으로 사용하면 런타임 오류를 더 쉽게 방지할 수 있습니다.

반환 타입을 타입 추론에 너무 의존하지 마세요.

다음으로 '반환 타입 추론에 너무 의존하지 않는 것'에 대해 살펴봅시다.

타입스크립트의 타입 추론은 강력하지만, 공개 함수의 반환 타입은 명시적으로 지정하는 것이 더 안전합니다. 이렇게 하면 향후 변경에 의한 영향을 최소화할 수 있습니다.

1// Bad
2function sum(a: number, b: number) {
3  return a + b;
4}

작은 함수에도 의도를 명확하게 표현하세요.

1// Good
2function sum(a: number, b: number): number {
3  return a + b;
4}

반환 타입을 명시하는 것은 API의 안정성을 높여줍니다.

unknown을 사용하여 입력 값을 안전하게 처리하세요.

다음으로 'unknown을 사용하여 외부 입력을 안전하게 받는 것'에 대해 살펴봅시다.

API, JSON 또는 사용자 입력과 같은 외부 입력에는 any 대신 unknown을 사용하세요. 이렇게 하면 모든 값이 검증되어 타입 안정성이 유지됩니다.

1// Bad
2function handleResponse(data: any) {
3  console.log(data.id);
4}

unknown을 사용하여 타입을 검증하는 방법은 다음과 같습니다.

 1// Good
 2function handleResponse(data: unknown): void {
 3  if (
 4    typeof data === "object" &&
 5    data !== null &&
 6    "id" in data
 7  ) {
 8    console.log((data as { id: number }).id);
 9  }
10}

unknown은 그대로 사용할 수 없으며, 검증이 필요한 타입입니다. 외부 입력을 처리할 때 특히 효과적입니다.

작은 타입을 조합해서 표현력을 높이세요.

다음으로 '작은 타입을 조합하여 표현력을 높이는 것'에 대해 살펴봅시다.

큰 타입을 한 번에 정의하면 가독성과 유지보수성이 저하됩니다. 타입을 의미 있는 단위로 나누고 필요할 때 조합해서 사용하세요.

 1type Id = number;
 2
 3type UserProfile = {
 4  id: Id;
 5  name: string;
 6};
 7
 8type UserWithStatus = UserProfile & {
 9  status: "active" | "inactive";
10};

타입을 컴포넌트처럼 취급하면 설계를 더 체계적으로 할 수 있습니다.

typeinterface

interface의 장점

typeinterface는 모두 타입을 정의할 수 있지만, 용도나 특성에서는 차이가 있습니다. 적절한 역할에 따라 구분해서 사용하면 타입 정의의 의도가 더 명확해집니다.

 1// Bad
 2type User = {
 3  id: number;
 4  name: string;
 5};
 6
 7type AdminUser = {
 8  id: number;
 9  name: string;
10  role: "admin";
11};

이처럼 공통 부분을 중복하면, 코드가 변경에 취약해집니다.

1// Good
2interface User {
3  id: number;
4  name: string;
5}
6
7interface AdminUser extends User {
8  role: "admin";
9}

interface는 확장(extends)이 필요한 설계에 적합하며, 객체의 '모양'을 표현하는 데 가장 좋습니다.

type의 장점

반면에 type은 더 표현력이 뛰어나 유니온·인터섹션 타입 등에 적합합니다.

1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4type ApiResponse<T> =
5  | { status: "success"; data: T }
6  | { status: "error"; message: string };

type은 상태, 옵션, 조합 등을 표현하는 데 적합합니다.

typeinterface 선택 가이드라인

일반적으로 객체 구조 및 계약에는 interface를, 유니온·인터섹션·타입 연산 등 더 표현력이 필요한 경우는 type을 사용하세요.

둘 다 비슷하게 동작하지만, 타입의 존재 이유가 더 잘 전달되도록 선택하는 것이 중요합니다.

타입을 문서처럼 취급하세요.

마지막으로 '타입을 문서로서 작성하는 것'에 대해 살펴봅시다.

좋은 타입 정의는 주석보다 더 많은 정보를 전달합니다. '타입만 보아도 사양을 이해할 수 있는 상태'를 목표로 하는 것이 중요합니다.

1type ApiError = {
2  code: number;
3  message: string;
4  retryable: boolean;
5};

이처럼 TypeScript의 주요 강점 중 하나는 타입 정의가 일종의 사양 문서로서 기능할 수 있다는 것입니다.

요약

타입스크립트의 베스트 프랙티스는 지나치게 엄격해지는 것이 아닙니다. 핵심은 타입으로 의도를 명확히 하고, 변화에 강한 코드를 작성하는 것입니다.

작은 규칙을 평소에 쌓아두면 ‘리뷰가 쉬워짐’, ‘버그가 줄어듦’, ‘미래의 나와 동료가 더 잘 이해함’ 등 장기적인 효과를 얻을 수 있습니다.

먼저 **‘이것을 타입으로 어떻게 표현할 수 있을까?’**라고 고민하는 습관이 고품질의 타입스크립트 코드로 이어집니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video