[패스트캠퍼스 프론트엔드 부트캠프 후기] TypeScript

vgotu99·2024년 11월 29일
post-thumbnail

패스트캠퍼스 프론트엔드 부트캠프 후기: TypeScript

TypeScript

TypeScript의 등장 배경, 필요성

  1. 자바스크립트의 한계
    자바스크립트는 웹 개발에서 가장 널리 사용되는 언어지만, 다음과 같은 단점을 가지고 있다:
    • 정적 타입 부재: 변수나 함수의 타입이 런타임에 결정되어 코드 작성 시 실수를 잡기 어렵다.
    • 규모 확장성 문제: 프로젝트가 커지면 코드의 가독성과 유지보수가 어려워지며, 타입 관련 오류가 빈번히 발생한다.
    • 개발 생산성 저하: IDE와 에디터에서 제공하는 코드 자동완성, 오류 탐지 등의 기능이 제한적이다.
  2. 타입스크립트의 등장 배경
    마이크로소프트가 개발한 타입스크립트는 자바스크립트의 단점을 보완하기 위해 등장했다.
    타입스크립트는 자바스크립트를 확장한 정적 타입 시스템과 최신 ECMAScript 기능을 제공한다.
    점점 복잡해지는 대규모 웹 애플리케이션을 안정적으로 개발할 수 있는 환경을 제공하기 위해 설계되었다.

React, Next.js와 같은 라이브러리 혹은 프레임워크와 함께 사용하여 대규모 프로젝트 진행 시 TypeScript의 타입이 빛을 발휘할 수 있다. 대규모 프로젝트 진행 시 React, Next.js와 같은 라이브러리 혹은 프레임워크는 컴포넌트 단위 개발을 진행하기에 로직과 UI가 분리되어있기도 하는 등 점점 더 작은 단위로 쪼개서 개발을 진행하는데 이때 타입을 이용하면 개발 단계에서 에러를 최대한 잡아가며 개발할 수 있다.

TypeScript를 사용해야하는 이유

  1. 정적 타입 시스템 도입
    코드 작성 시점에 타입 오류를 미리 확인할 수 있어 런타임 오류를 줄여준다.
    타입 정의를 통해 코드의 의도를 명확히 표현할 수 있다.
    협업 시 타입 정보가 코드의 문서화 역할을 하여 커뮤니케이션 비용을 줄인다.
  2. 개발 생산성 향상
    에디터와 IDE에서의 자동완성, 코드 리팩토링, 오류 탐지가 더 강력해진다.
    예: 함수 호출 시 인자의 타입을 자동으로 알려주거나, 올바른 사용법을 제안한다.
  3. 유지보수성 개선
    타입 정의가 있기 때문에 시간이 지나도 코드를 이해하기 쉽다.
    대규모 프로젝트에서도 안정성을 유지하며 기능을 추가할 수 있다.
  4. 최신 자바스크립트 지원
    타입스크립트는 최신 ECMAScript 표준을 기반으로 동작하므로, 최신 문법을 사용해 작성한 코드를 호환성 있게 트랜스파일할 수 있다.
    오래된 브라우저에서도 최신 기능을 사용할 수 있는 환경을 제공한다.
  5. 대규모 프로젝트에 적합
    코드의 구조가 복잡해질수록 타입스크립트의 장점이 더욱 빛난다.
    많은 팀원과 협업하는 프로젝트에서 코드 품질 유지에 큰 도움이 된다.
  6. 강력한 커뮤니티와 생태계
    타입스크립트는 지속적으로 업데이트되며, 많은 라이브러리와 프레임워크(React, Angular, Node.js 등)에서 기본적으로 지원한다.
    대규모 커뮤니티와 풍부한 자료 덕분에 학습 및 적용이 용이하다.

Types


// Boolean
let boolean: boolean;
let falseBoolena: boolean = false;

// Number
let number: number;
let integer: number = 6;
let float: number = 1.2345;

// String
let string: string;
let firstName: string = "Doe";

// Union
let union: (string | number )
union = 'hi'
union = 123
// union = false // boolean 타입은 정의해주지 않았으므로 boolean 값 할당 불가능

// Array
  // 한가지 타입만 가지는 배열
  let names1: string[] = ["John", "Kim"];
  let names2: Array<string> = ["John", "Kim"];

  // 여러 타입을 가지는 배열(유니언 타입 사용)
  let array1: (string | number)[] = ["John", 1, 2];
  let array2: Array<string | number> = ["John", 1, 2];

  // 여러 타입을 단언할 수 없는 경우는 any를 사용
  let someArray: any[] = ["John", 1, [], {}, false];

// Interface, Type

// 읽기 전용 배열 생성(readonly, ReadonlyArray)
let stringArray1: readonly string[] = ["A", "B"];
let stringArray2: ReadonlyArray<string> = ["A", "B"];
let numberArray1: readonly number[] = [1, 2];
let numberArray2: ReadonlyArray<number> = [1, 2];
// 읽기 전용 배열이기 때문에 값을 추가하거나 수정하는 등의 배열 메소드를 사용할 수 없다.
  // stringArray1.push('C')
  // stringArray2[0] = 'C'
  // numberArray1.push(3)
  // numberArray2[0] = 3


// Tuple
let tuple1:[string, number]
tuple1 = ['a', 1]
// tuple 타입은 배열의 인덱스마다 타입을 정의해주는 방식이므로 특정 인덱스에 해당하는 타입의 값만 넣어줄 수 있고 지정해주지 않은 인덱스에는 값을 넣을 수 없다
  // tuple1 = [1, 'a']
  // tuple1 = ['a', 1, 1]

let tuple2: [string, number]
tuple2 = ['a', 1]
tuple2.push(2) // push 메소드를 이용해서 tuple 타입 배열에 값을 넣어줄 수는 있다.
// tuple2.push(false) // 하지만 처음에 정의해준 타입 외의 타입은 추가할 수 없다.
console.log(tuple2) // [ 'a', 1, 2 ]

let users: [number, string][]
users = [[1, 'John'], [2, 'Doe']]

// Any
// 서드파티 라이브러리 사용 시 해당 라이브러리의 타입을 모두 알고 사용할 수는 없으니 그때 사용! 평소에는 가급적 사용xxx
let any: any = 'abc'
any = 1
any = []
any = false

// Unknown
let unknown: unknown = 'abc';
// let string1: string = unknown // unknown 타입으로 정의된 값은 다른 변수에 할당할 수 없다.
let string2: string = unknown as string // as type을 사용하면 unknown 타입으로 정의된 값도 다른 변수에 할당할 수 있다.

// Object
let obj: object = {}
let arr: object = []
// let nul: object = null // 실제로 null은 object가 아니기 때문에 object 타입으로 정의할 수 없다. 다만 tsconfig.json의 "strict": false로 변경해주면 null 값에 object 타입을 정의할 수 있게된다.
let date: object = new Date()

const obj1: {id: number, title: string, description: string} = {
  id: 1,
  title: 'hello',
  description: 'world'
}

// Function
let func1: (arg1: number, arg2: number) => number
// 매개변수와 리턴값에 타입을 정의할 수 있다.
func1 = (x, y) => {
  return x * y
}

let func2: () => void
// 리턴이 없는 경우 void 타입으로 정의해준다.
func2 = () => {
  console.log('func2')
}

// Void
const greeting = (): void => {
  console.log('hi')
}

const hi: void = greeting()
console.log(hi) // undefined

// Never
const throwError = (): never => {
  throw new Error('error')
}

const keepProcessing = (): never => {
  while (true) {
    console.log('keep!!')
  }
}

Type Aliase & Interface

// interface
interface Animal {
  name: string
}
// extends를 이용한 interface 확장, class형 문법 느낌
interface Bear extends Animal {
  honey: boolean
}
// 같은 이름으로 재선언하여 Animal의 모든 타입들을 병합할 수 있다.
interface Animal {
  cute: boolean
}

const bear: Bear= {
  name: 'honey bear',
  honey: true,
  cute: true
}


// ==============================================================================

// type
type Car = {
  name: string;
}
// intersection operator를 이용한 타입 결합, 확장 처럼 보이지만 사실은 다른 타입과 결합하는 것
type Porsche = Car & {
  fancy: boolean;
}
/* 재선언이 불가능하다. 즉, 선언 병합 불가능
type Car = {
   fast: boolean
}
*/

const boxster: Porsche = {
  name: 'boxster',
  fancy: true
}

Generics

// function getArrayLength(arr: number[] | string[] | boolean[]): number {
//   return arr.length
// }

function getArrayLength<T>(arr: T[]): number {
  return arr.length
}

const array1 = [1, 2, 3]
const array2 = ['a', 'b', 'c']
const array3 = [true, false, true]

getArrayLength<number>(array1)
getArrayLength<string>(array2)
getArrayLength<boolean>(array3)

// ================================================================

interface Vehicle<T> {
  name: string
  color: string
  option: T
}

const car: Vehicle<{price: number}> = {
  name: 'car',
  color: 'red',
  option: {
    price: 1000
  }
}

const bike: Vehicle<boolean> = {
  name: 'bike',
  color: 'green',
  option: true
}

// ================================================================

const makeArr = <X, Y>(x: X, y: Y): [X, Y] => {
  return [x, y]
}

const arr1 = makeArr<number, number>(4, 5)
const arr2 = makeArr<string, string>('a', 'b')

const makeArr1 = <X, Y= string>(x: X, y: Y): [X, Y] => {
  return [x, y]
}

const arr3 = makeArr1<string>('a', 'b')

const makeFullName = <T extends {firstName: string, lastName: string}>(obj: T) => {
  // <T extends 어쩌구> = 어쩌구라는 매개변수는 반드시 호출할 때 인자로 포함되어야한다. 라는 뜻이다.
  return {
    ...obj,
    fulleName: obj.firstName + ' ' + obj.lastName
  }
}

makeFullName({firstName:'John', lastName:'Doe', location:3})

Utility-Types

// Partial
interface Address {
  email: string;
  address: string;
}
// Partial<interface>는 interface이 가진 모든 속성을 선택 속성(?)으로 변환하여 일부 속성만 포함하거나 아무 속성도 포함하지 않을 수 있게 해준다.
const me: Partial<Address> = {};
const you: Partial<Address> = { email: "jobh@naver.com" }; // 따라서 선택 속성으로 변환되었기 때문에 여기서 할당된 email은 Address.email: string 타입이 아닌 email?: string | undefined가 된다.
const all: Address = { email: "jobh@naver.com", address: "asd" };

// Pick
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
// Pick utility type을 사용해서 특정 interface 안에서 원하는 속성의 type만 선택해서 새로운 타입으로 만들어줄 수 있다.
type TodoPreview = Pick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
  title: 'clean room',
  completed: false
}


// Omit
interface Todo1 {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number
}
// Omit은 Pick과 반대로 특정 interface 안에서 원하지 않는 속성의 type만 선택해서 빼고 남은 속성들의 type으로 이루어진 새로운 타입을 만든다.
// Omit<interface, key | key>
type TodoPreview1 = Omit<Todo1, 'description'>

const todo1: TodoPreview1 = {
  title: 'clean room',
  completed: false,
  createdAt: 123123123,
}


// Exclude
type Fruit = string | number | boolean
// Exclude를 이용해서 타입이 가지고 있는 타입들 중 특정 타입을 제거하고 남아있는 새로운 타입들을 반환한다
// Omit과 다른 점은 Exclude는 type 자체를 인자로 받고 Omit은 key를 인자로 받는 점이다. 따라서 Omit은 객체 타입에 있는 특정 프로퍼티 타입을 제외하는 것이고 Exclude는 유니온 타입에 있는 특정 타입을 제외하는 것이다.
// Exclude<interface, type | type>
type newFruit = Exclude<Fruit, boolean>

const fruit1: newFruit = 'kiwi'
const fruit2: newFruit = 1
// const fruit3: newFruit = true

interface ABC {
  a: string;
  b: number;
  c: boolean;
}


// Required
type User = {
  firstName: string
  lastName?: string // key?: value는 선택 속성으로 User type을 사용할 때 있어도 되고 없어도 되는 property로 만들 수 있다.
}

let firstUser : User = {
  firstName: 'John'
}
// Required<Type>으로 Type에 옵셔널한 선택 속성이 있는 경우에도 모든 속성을 필수로 포함하도록 강제할 수 있다.
let secondUser: Required<User> = {
  firstName: 'Kim',
  lastName: 'Jun' // lastName?: string 이었지만 Required 유틸리티 타입으로 인해 필수 속성으로 강제했기에 반드시 포함해야한다.
}


// Record  |  Record<Keys, Type>
// 키가 Keys이고 값이 Type인 객체 type을 구성한다. type의 속성을 다른 type에 매핑하는데 사용함
interface CatInfo {
  age: number
  breed: string
  name: CatName
}

type CatName = 'tigger' | 'boris' | 'mordred'

const cats: Record<CatName, CatInfo> = {
  tigger: {age:3, breed: 'korean shorhair', name: 'tigger'},
  boris: {age: 5, breed: 'maine coon', name: 'boris'},
  mordred: {age: 13, breed: 'british shorthair', name: 'mordred'},
}


// ReturnType  |  ReturnType<functionType>
// functionType의 반환 타입으로 구성된 타입을 만듭니다.
type T0 = ReturnType<() => string>  // T0은 string type
type T1 = ReturnType<(s: string) => void>  // T1은 void type

const fn = (str: string) => {
  return str
}

const a: ReturnType<typeof fn> = 'hello'
// const b: ReturnType<typeof fn> = true  // typeof fn은 string type이므로 오류 발생!

Mapped Types

// Mapped
// Mapped type은 중복을 피하기 위해 다른 타입을 바탕으로 새로운 타입을 생성할 수 있다.
// type이 다른 type에서 파생되고 동기화 상태를 유지해야 하는 경우에 특히 유용하다.
// { [T in unionType] : type_of_value }
// map method처럼 하나씩 순회를 하며 객체 타입을 만들어주는 느낌!
type AppConfig = {
  username: string
  email: string
  imgURL: string
  phone: number
}

type AppPermissions = {
  changeUsername: boolean
  changeEmail: boolean
}
// 위의 AppPermissions는 AppConfig에 새로운 타입인 a: string을 추가하면 일일히 changeA: boolean 타입을 추가해줘야한다. 따라서 아래 처럼 mapped type을 이용해서 AppConfig 타입에 새로운 타입이 추가되면 AppPermissionsMapped 타입에 새로운 타입이 추가되도록 변경해주면 유지보수에 좋다!
type AppPermissionsMapped = {
  [K in keyof AppConfig as `change${Capitalize<K>}`]: boolean
}
/* 위 OptinalType은 아래와 같은 형태의 객체 타입이 된다.
type AppPermissionsMapped = {
  chageUsername: boolean
  chageEmail: boolean
  chageImgURL: boolean
  chagePhone: boolean
}
*/

type Users = "John" | "Han" | "Kim";

type UserAge = { [T in Users]: number };
/* 위 UserAge는 Mapped type을 이용해서 아래와 같은 형태의 객체 타입이 된다.
type UserAge = {
  John: number,
  Han: number,
  Kim: number
}
*/

const userFirstNameInfo: UserAge = {
  John: 25,
  Han: 31,
  Kim: 22,
  // Kim: true
  // Lee: 27
  // key는 반드시 Users에 속하는 타입이어야하고 value는 반드시 UserFirstNames에 속하는 타입이어야한다.
};


type DeviceFormatter<T> = {
  [K in keyof T]: T[K];
};

type Device = {
  manufacturer: string;
  price: number;
};

const iphone: DeviceFormatter<Device> = {
  manufacturer: "apple",
  price: 300,
};


type OptinalType<T> = {
  [K in keyof T]? : T[K]
  // 선택 속성으로 만드는 방법 1
}
/* 위 OptinalType은 아래와 같은 형태의 객체 타입이 된다.
type OptinalType = {
  a?: string,
  b?: number,
  c?: boolean
}
*/

type Type = {
  a: string,
  b: number,
  c: boolean
}

const x:OptinalType<Type> = {
  a: 'a',
  b: 3,
}

type OptinalType1<T, P extends keyof T> = Omit<T, P> & Partial<Pick<T, P>>
// 선택 속성으로 만드는 방법 2
/* 위 OptinalType1은 아래와 같은 형태의 객체 타입이 된다.
type OptinalType1 = {
  a: string,
  b: number,
}
Omit<T, P>로 T 타입에서 P 속성을 제거한다.
Pick<T, P>로 T 타입에서 P 속성만 추출해서 새로운 타입으로 만들어준다.
Partial<newP>로 P 속성을 선택 속성으로 변경한다 => c?: boolean
*/

const yxz: OptinalType1<Type, 'c'> = {
  a: 'a',
  b: 2,
  // c는 생략가능
}

패스트캠퍼스 프론트엔드 부트캠프 후기 글이었습니다.

0개의 댓글