Jaeilit

타입스크립트을 써야하는 이유 본문

TIL

타입스크립트을 써야하는 이유

Jaeilit 2025. 12. 13. 21:08
728x90

배경

JS로만 되어있는 프로젝트를 담당하게 되었습니다. 언어 레벨에서는 프로젝트의 안전성을 생각해서 TS를 도입해야한다고 생각했고 제안을 했습니다. 팀 리더분께서는 처음에 부정적인 시선으로 보셨고, 그래도 제 의견을 조금 고려해주셔서 발표 세션을 준비하라고 하셨습니다.

이 내용은 내부적으로 설득에 이용했던 내용을 회사 코드를 걷어내고 조금 수정한 수정 본입니다.

런타임 오류 방지

자바스크립트의 한계는 타입 관련 오류를 실행 전에 잡아낼 수 없다는 점입니다.

자바스크립트는 동적 타입 언어로, 변수의 타입이 실행 시점(런타임)에 결정됩니다. 예를 들어, 숫자를 기대하는 함수에 문자열을 전달해도 코드를 실제로 실행하기 전까지는 이러한 오류를 발견할 수 없습니다.

이런 한계를 극복하고자 자바스크립트의 슈퍼셋으로 타입스크립트가 등장했습니다. 타입스크립트는 코드 작성 시점과 트랜스파일 시점에서 타입 오류를 사전에 잡아내어 런타임 오류를 방지하는데 도움을 줍니다.

예시 1)

// JavaScript (런타임 오류 가능)
function add(a, b) {
  return a + b;
}
add(1, "2"); // "12" (원치 않는 결과)
// TypeScript (코드 작성 시점에 오류 감지)
function add(a: number, b: number): number {
  return a + b;
}
add(1, "2"); // ❌ 오류: 'string' 형식을 'number'에 할당할 수 없음

자동완성 기능

자동완성과 IntelliSense는 개발 생산성을 크게 향상시키는 기능입니다. 순수 JavaScript에서도 IDE가 어느 정도 타입 추론을 제공하지만, 추론이 불가능한 경우 `any` 타입으로 표시됩니다. `any` 타입은 "어떤 타입이든 될 수 있다"는 의미로, 이는 곧 타입 안전성이 전혀 보장되지 않는다는 뜻입니다.

제네릭을 활용한 타입 재사용

TypeScript의 제네릭(Generic)은 타입을 변수처럼 사용할 수 있게 해주는 기능입니다. 이를 통해 재사용 가능한 타입 구조를 만들 수 있으며, 특히 백엔드와 협업 시 API Response와 Request Parameter를 타입 안전하게 관리할 수 있습니다.

API 공통 구조 정의

백엔드 API는 보통 일관된 응답 형식을 가집니다.
제네릭을 사용하면 이 공통 구조를 한 번만 정의하고 여러 API에서 재사용할 수 있습니다.

// api.ts - 모든 API가 공유하는 기본 구조
export interface APIResponse {
    resData: T;      // 실제 데이터 (API마다 다름)
    resCode: string; // 응답 코드
    resMsg: string;  // 응답 메시지
}

export interface APIFnCallback {
    onSuccess: (_data: T) => void;
    onError: (_data: T) => void;
}

 

제네릭 <T>의 의미:

  • T는 Type Parameter로, 실제 사용 시점에 구체적인 타입으로 대체됩니다
  • 각 API마다 다른 데이터 구조를 유연하게 적용할 수 있습니다
  • 타입 안전성을 유지하면서 코드 재사용성을 높입니다

제네릭 활용의 장점

  1. 타입 재사용성
    • APIResponse<T> 하나로 모든 API 응답 타입 커버
    • 새로운 API 추가 시 데이터 타입만 정의하면 됨
  2. 자동완성 지원
    • response.resData. 입력 시 해당 API의 모든 필드 확인 가능
    • API 명세를 매번 확인할 필요 없음
  3. 타입 안전성
    • 잘못된 필드 접근 시 컴파일 단계에서 오류 감지
    • 리팩토링 시 영향받는 모든 코드를 IDE가 표시
  4. 유지보수 용이
    • 공통 구조 변경 시 한 곳만 수정
    • API 명세 변경 시 타입만 수정하면 관련 코드에서 오류 표시

전체 예시코드)

// api.ts
// api response format
export interface APIResponse<T> {
    resData: T;
    resCode: string;
    resMsg: string;
}

export interface APIFnCallback<T> {
    onSuccess: (_data: T) => void;
    onError: (_data: T) => void;
}

// login.ts
import type { APIFnCallback, APIResponse } from '../common/api';

export interface LoginAPIParams {
    id: string;
    password: string;
}

export interface LoginAPISuccess {
    isFirstLogin: boolean;
    isPasswordExpired: boolean;
    accessToken: string;
}

export interface LoginAPIError {
    loginAttemptCnt: number;
    remainingTime: string;
    accountStartDate: string;
}

// data
export type LoginAPIResponseData = LoginAPISuccess & LoginAPIError;

// response
export type LoginAPIResponse = APIResponse<LoginAPIResponseData>;

// params
export type LoginAPIFetchFn = LoginAPIParams & APIFnCallback<LoginAPIResponse>;

 

 

JS Doc 처럼 프로퍼티 설명도 추가할 수 있어 구조를 한 눈에 파악할 수 있는 장점도 있습니다.

TS인터페이스에서 JSdoc 활용

리팩터링 및 유지보수 생산성 향상

any 타입은 타입 검사를 하지 않겠다 라는 의미를 가진 타입입니다. 그 만큼 TS를 쓴다고 해도 any로 추론되는 속성들이 많으면 런타임에 오류가 날 확률이 그 만큼 증가하게 됩니다.

any를 막기 위해 eslint 설정도 할 수 있지만 너무 과한 타입가드는 오히려 타입을 잡기 위해 더 많은 시간을 쏟아야 하는 경우도 있습니다.

미리 사전에 타입을 정의해두었다면 API 명세가 바뀌거나, 기존 FE 내부 코드에서 함수나 객체 등이 바뀐다 하여도 코드 작성 시에 타입 오류를 잡아내서 런타임 오류를 잡아 낼 수 있습니다.

코드 작성 시에 잡아 내지 못했다면 프로젝트 build 시에 컴파일 과정에서 tsc(typescript compiler)가 잡아내기 때문에 프로젝트가 더욱 더 안전성이 향상 됩니다.

인터페이스 타입 설계

최근 프론트엔드의 컴포넌트를 만드는 방식에는 여러 패턴들이 있습니다. 그 중에서 UI 관점에서 봤을 때 컴포넌트 설계 시에 작은 단위의 컴포넌트를 만들고 이들과 조합하여 또 하나의 UI를 만드는 식으로 아토믹 패턴과 유사한 방식으로 디자인 시스템을 구축하는 것이 특징입니다.

예를 들어 input, label을 구별하여 작성하고 inputForm이라는 label과 input이 합쳐진 컴포넌트를 작성하는 것입니다. 리액트에서는 각 컴포넌트 단위에서 필요한 props 들을 정의하고 이 컴포넌트를 가져다 쓰는 컴포넌트에서 하위 타입을 확장(extends)시켜 바텀 업 방식의 설계를 할 수 있습니다.

 

 

예시3)

input + label 타입 설계

// label/type.ts
export type LabelColor = 'light' | 'white';

export interface LabelProps {
    id: string;
    label: string;
    className?: string;
    color?: LabelColor;
}

// input/type.ts
import type { InputTypeHTMLAttribute } from 'vue';
import type { LabelProps } from '../label/type';

type InputFieldVariant = 'fill' | 'outline';

// input props
export interface Props {
    // label
    label: string;

    type?: InputTypeHTMLAttribute;

    // error
    isError?: boolean;
    errorMessage?: string;

    variant?: InputFieldVariant;

    // onchange
    onInput?: (_e: Event) => void;
}

export interface PasswordIconProps {
    show: boolean;
    onVisible: () => void;
}

// label + input props
export type InputFieldProps = Omit<Props, 'variant'> &
    Omit<LabelProps, 'color'> & {
        showPassword?: PasswordIconProps;
        labelColor?: LabelProps['color'];
        inputFieldVariant?: Props['variant'];
    };
// button/type.ts

export type ButtonColor = 'primary';
export type ButtonVariants = 'fill' | 'line';
export type ButtonSize = 'sm' | 'md' | 'lg';

export interface ButtonProps {
    // color
    color?: ButtonColor;

    // variants
    variants?: ButtonVariants;

    // size
    size?: ButtonSize;

    onClick?: (_event: MouseEvent) => void;

    disabled?: boolean;

    className?: string;
}

// button.vue
<script setup lang="ts">
import type { ButtonProps } from './type';

const { color = 'primary', size = 'md', variants = 'fill', onClick, className, disabled } = defineProps<ButtonProps>();

const buttonPrefix = [size, `${variants}-${color}`].map((atr) => `button-${atr}`);
</script>

<template>
    <button :disabled="disabled" :class="['button', className, ...buttonPrefix]" @click="onClick">
        <slot />
    </button>
</template>

<style lang="scss" scoped>
// default button styles
.button {}


// variants-color
.button-fill-primary {}

.button-line-primary {}

// size
.button-sm {}

.button-md {}

.button-lg {}
</style>

백엔드와의 타입 동기화

API code generator는 백엔드의 명세를 기반으로 프론트엔드의 타입스크립트 코드를 자동으로 생성해주는 툴입니다.

대표적으로는 swagger-typescript-api, open api generator 가 있습니다.

기존에는 API 명세를 보고 타이핑하면서 작업을 했었지만 이 툴을 도입하게 된다면 수작업을 제네레이터가 더 빠르고 더 정확하게 생성해주기 때문에 생산성이 향상 됩니다. 또한 개발 단계에서는 백엔드에서도 API 명세가 바뀌는 경우가 가끔 있는데 이 경우에도 프론트엔드에서 다시 생성하여 즉시 동기화할 수 있으므로 유지보수 효율도 높아집니다.

swagger-typescript-api 예시

728x90