TypeScript 톺아보기
따로 TypeScript를 공부하지는 않고, 개발을 하면서 모르는 부분을 검색하며 학습을 했습니다. 개발을 할 때는 필요하면 type을 바로 추가하거나, 오류가 나는 부분은 적당하게 맞춰서 type을 작성하다보니, 크게 어려움을 느끼지 못 했습니다.최근에 오픈소스를 기
왜 톺아보는가?
따로 TypeScript를 공부하지는 않고, 개발을 하면서 모르는 부분을 검색하며 학습을 했습니다. 개발을 할 때는 필요하면 type을 바로 추가하거나, 오류가 나는 부분은 적당하게 맞춰서 type을 작성하다보니, 크게 어려움을 느끼지 못 했습니다.
최근에 오픈소스를 기여하게 되면서, 어능정도 정형화된 컨벤션과 type을 관리하는 방법들을 잘 소화하지 못 해 삽질 하게 되면서 한번 톺아봐야겠다 라고 결론을 내렸습니다.
TypeScript
왜 사 용하는 걸까?
프로젝트를 만들어서 개발을 하게되면, JavsScript 또는 TypeScript로 언어를 선택하게 됩니다. 보통 TypeScript는 타입을 지정해줘야하는 번거로움 때문에 JavaScript를 사용하는 사람도 많습니다.
'TypeScript' 는 JavaScript 기반의 정적 타입 문법을 추가한 프로그래밍 언어입니다.
'JavaScript' 는 대표적인 동적 타입 언어입니다. 배우기 쉽고 유연한 장점이 있지만, 동시에 런타입 에러 발생 가능성을 가지고 있습니다. 여러 의존성이 겹친 규모가 큰 프로젝트에서는 에러가 어디서 발생하는지 알기가 쉽지 않습니다.
하지만, TypeScript는 정적 타입 언어로 변수의 타입을 명시적으로 지정해서 사용합니다.
컴파일 단계에서 타입 체크를 하여 코드 작성 중 타입 오류를 방지하고, 타입 체킹 기능인 '타입 어노테이션' 을 활용하여 에러를 빠르게 처리할 수 있습니다.
어떻게 설정할까?
TypeScript를 사용하기 위해서는 tsconfig.json이 필요합니다.
컴파일 옵션 을 설정 할 수 있으며, 여러가지 오류의 범위를 지정할 수 있습니다.
다음 동작원리를 보면서 왜 이 과정이 필요한지 알아봅시다.
출처: 한 입 크기로 잘라먹는 타입스크립트 | 더 자세한 내용
동작원리
JavaScript의 컴파일 과정을을 보면 코드가 AST로 변환하고 AST를 다시 바이트 코드로 변환되어서 이렇게 변환된 바이트코드를 컴퓨터가 실행하게 됩니다.
이게 대다수의 프로그래밍 언어의 동작 방식입니다.
그러면 TypeScript는 어떻게 동작할까요?
마찬가지로 TypeScript도 AST로 변환합니다. 그리고 AST에서 Type Checking을 하게 됩니다.
성공 시 JavaScript의 컴파일 과정으로 변환이 됩니다. 실패 시 컴파일이 종료되구요.
따라서 TypeScript 컴파일 시 미리 타입 체킹 이 되니 보다 안전하게 컴파일이 가능합니다.
TypeScript 흔히 잘 모르는 것들
타입 계층도
위 계층도를 알고 계시면, 타입을 상속하거나 다른 타입에 할당 가능한 지 알 수 있습니다.
슈퍼타입(상위 타입) 은 더 넓은 범위의 타입이고, 서브 타입(하위 타입) 은 더 구체적인 타입입니다.
업 캐스팅 : 서브 타입의 값을 슈퍼타입의 값으로 취급하는 것 ( 대부분 가능 ) 다운 캐스팅 : 슈퍼 타입의 값을 서브 타입의 값으로 취급하는 것 ( 대부분 불가능 )
타입 단언과 타입 좁히기 그리고 유니온 타입
여러 복잡한 타입 속에서 타입 추론을 하다보면 내부 코드가 타입 추론 을 하지 못하고 빨간색 에러를 나타나게 되는데 이 때 AI에게 도움을 받으면 옵셔널 체이닝(?.)과 as any로 그냥 에러를 급하게 막게 됩니다.
협업 시 타입 관련 오류를 찾지 못 하거나, 이상한 값을 불러와서 오히려 생산성을 저하 시킬 수 있습니다.
그래서 타입 단언, 타입 좁히기 그리고 유니온 타입 을 적절히 사용하여 타입을 관리하면 클린 코드 측면에서 효율적으로 코드를 작성할 수 있습니다.
// 타입 단언 (as 키워드)const element = document.getElementById('myId') as HTMLInputElement;
// 타입 좁히기function processValue(value: string | number) { if (typeof value === 'string') { // 이 블록에서는 value가 string 타입으로 좁혀짐 return value.toUpperCase(); } // 이 블록에서는 value가 number 타입 return value.toFixed(2);}
// 유니온 타입과 함수 오버로딩을 통한 타입 좁히기function getData(id: string): string;function getData(id: number): number;function getData(id: string | number): string | number { if (typeof id === 'string') { return `Data for ${id}`; } return id * 2;}
// 사용자 정의 타입 가드function isString(value: unknown): value is string { return typeof value === 'string';}Q. 제네릭 타입 변수를 사용하면 보다 깔끔하게 코드 작성이 가능하지 않나요? A. 제네릭은 미리 정해진 타입 구조에서 타입만 바뀌는 경우 적절하고, 타입 좁히기는 런타입에 실제 값을 보고 타입을 판단해야 하는 경우가 적절하여, 둘 다 상황에 맞게 사용해야 합니다.
인터페이스와 타입
// 인터페이스 - 확장 가능interface User { name: string; age: number;}
interface User { email: string; // ✅ 선언 병합 가능}
interface Admin extends User { role: string;}
// 타입 별칭 - 더 유연함type UserType = { name: string; age: number;}
// type UserType = { email: string } // ❌ 중복 선언 불가
type AdminType = UserType & { role: string;}
// 유니온 타입, 원시값 등은 type만 가능type Status = 'loading' | 'success' | 'error';type ID = string | number;둘 다 타입을 정의하지만, 미묘한 차이가 있습니다.
확장 가능성이 있는 객체 구조 : interface 유니온, 교차 타입, 복합 타입: type
프로젝트 마다 타입을 어떻게 관리 할 지 다르다고 합니다. 저는 interface로 관리하고, 유니온 타입이 필요하다면 type으로 선언하는 편 입니다 !