Frontend/Today I Learned

[TS] TypeScript 시작하기

joycie416 2024. 9. 24. 21:04

React+vite에 typescript로 작성하려고 한다.

 

Javascript와의 차이점과 typescript의 특징에 대해 약간 정리한 후 코드 예시를 살펴보려고 한다.

 

1. Typescript = Javasript + 정적 type 시스템

먼저 타입 시스템이란 프로그래밍 언어가 프로그램에서 가질 수 있는 타입을 이해하는 방법에 대한 규칙 집합이다. 타입이란 null, undefined, boolean, string, number 등을 의미한다.

 

동적 타입 언어는 프로그램을 실행한 이후에 변수, 함수의 타입이 결정되는 언어이며 JS는 동적 타입 언어이다. 또한 타입이 고정되지 않아서 같은 벼수에 여러 타입의 값을 자유롭게 할당 할 수 있다.

let ex = '문자열'
ex = 1

 

TS는 JS와 달리 정적 타입 언어이다. 정적 타임 언어는 프로그램이 실행되기 전에 모든 변수와 표현식의 타입을 확인하고 고정하는 방식이다. 즉 위 예시처럼 타입을 자유롭게 변경할 수 없으며, 이를 통해 프로그램의 오류를 미리 발견할 수 있다.

 

특징 JavaScript TypeScript
타입 시스템 동적 타임 정적 타입
컴파일 필요 없음 JavaScript로 컴파일 필요
(프로젝트가 대형화되면 컴파일 시간이 길어짐)
개발 경험 유연하고 빠르게 시작 가능 코드 완성, 타입 검사 등으로 향상된 개발 경험
프로젝트 규모 작은 프로젝트에 적합 대규모 프로젝트에 적합
런타임 오류 런타임에 타입 오류 발견 가능 컴파일 타입에 타입 오류 발견 가능
학습 곡선 낮음 높음
도구 지원 광범위한 도구와 라이브러리 지원 JavaScript 생태계의 모든 도구와 라이브러리 지원
브라우저 지원 모든 브라우저에서 기본적으로 지원 트랜스파일된 JavaScript가 모든 브라우저에서 지원

 

TS는 타입을 사용해 우리 코드를 대신 분석해주며, 개발 과정에서 에러를 잡을 수 있게 도와주므로 프로젝트 대형화, 유지보수에 용이하다.

 

2. TS의 동작 원리

TS는 컴파일 과정에서 문법, 타입 체크 등을 수행한다. 여기서 컴파일은 쉽게 말하면 A언어를 B언어로 변환화는 과정을 말한다. (TS는 JS로 100% 컴파일된다.) TS는 크게 6개 과정을 따라 타입을 확인한다.

 

  1. 컴파일 시작: tsc 명령어를 사용해 타입스크립트 컴파일러를 실행합니다. 이때 `tsconfig.json` 파일을 참고하여 어떤 파일을 컴파일할지, 어떤 옵션을 사용할지 설정합니다.
  2. 파일 로드: 컴파일러가 모든 입력 파일과 import된 파일을 로드합니다.
  3. 코드 분석: 코드를 읽어들여 프로그램 구조를 나타내는 AST(구문 트리)를 만듭니다.
  4. 심볼 테이블 생성: 코드를 분석하여 변수와 함수 등 모든 요소의 관계를 정리한 심볼 테이블을 만듭니다.
  5. 자바스크립트 코드로 변환: AST를 바탕으로 타입스크립트 코드를 자바스크립트 코드로 변환합니다.
  6. 타입 검사: 컴파일러가 코드를 검사하여 타입 오류를 찾습니다. 오류가 없다면 최종 자바스크립트 파일을 생성합니다.
꼭 기억해야하는 것은 tsc(TypeScript compiler)가 코드를 분석해준다는 것이다. 즉 TS는 작성된 코드를 컴파일 타임에 분석한다. 이후 JS로 100% 컴파일되어 JS와 전부 동일하게 동작한다.

 

3. React + TS 시작하기

처음에 미리 말했듯이 React + Vite + TS 로 프로젝트를 생성할 것이다.

 

먼저 이전에 JS 프로젝트를 생성한 것처럼 시작하면 된다.

// yarn
yarn create vite my-app-name --template react-ts
cd my-app-name
yarn
yarn dev

//npm
npm create vite my-app-name --template react-ts
cd my-app-name
npm install
npm run dev

 

거의 JS를 이용한 때와 같다. 차이점은 아래 파일들과 jsx가 tsx로 바뀌었다는 것이다.

 

4. 타입 선언하기

TypeScript인 만큼 이제 타입을 선언해보자.

 

먼저 타입이란 값이 가진 property와 method 등을 참조하는 방법이다. 쉽게 말해 이 값이 어떤 속성을 가지로 있는지 표현하는 방법이다.

type Profile = {
  id: string;
  name: string;
  age: number;
  isMarried: boolean;
}

 

`string`, `number`처럼 이미 정해진 타입(원시타입) 말고도 우리가 직접 타입을 정의할 수도 있다.

예를 먼저 살펴보자.

function add(a: number, b: number): number {
  return a + b;
}

const sum: number = add(1, 2);
console.log(sum); // 3

 

`add`는 두 `number` 타입 parameter `a,b`를 받아서 `number` 타입 값을 반환해준다. 만약 `add`에 `string` 타입 argument를 넣게 되면 에러가 발생할 것이다.

 

위에 사용한 것처럼 일반 변수를 타입과 함께 할당하려면 아래와 같이 하면 된다.

let isTrue: boolean = true;
let num: number = 1;
let str: string = 'I am string';
let undefin: undefined = undefined;
let nul: null = null;


let any: any = 123;
any = 'play game';
any = {};
any = null;

const arr: any[] = [1, true, 'typescript'];

 

특정 타입으로 이루어진 배열 타입으로 선언하려면 아래와 같다.

let fruits: string[] = ["Apple", "Banana", "Mango"];
let fruits: Array<string> = ["Apple", "Banana", "Mango"];

let oneToSeven: number[] = [1, 2, 3, 4, 5, 6, 7];
let oneToSeven: Array<number> = [1, 2, 3, 4, 5, 6, 7];

let array: (string | number)[] = ["Apple", 1, 2, "Banana", "Mango", 3];
let array: Array<string | number> = ["Apple", 1, 2, "Banana", "Mango", 3];

let someArr: any[] = [0, 1, {}, [], "str", false];

 

여기서 `|`(유니온 타입)는 '또는' 이라고 생각하면 되는데, `(string | number)[]` 이것은 배열의 원소가 `string` 또는 `number` 이어야 한다는 것을 의미한다. `any`는 단어 그대로 어떤 한 타입도 모두 가능한 타입이며 이때는 거의 JS와 같은 경우이다.

let union: (string | number);
union = 'Hello World';
union = 777;
union = false; // Error

 

이 외에도 `readonly`, `Tuple`, `enum`, `object`, `void`, `unkown`, `never` 등이 있다.

 

위처럼 정해진 형식으로만 타입을 지정할 수 있는 것은 아니다. 우리가 직접 타입을 새롭게 만들 수도 있다.

//interface

interface IUser {
  name: string;
  age: number;
  isVaild: boolean;
}

let userArr: IUser[] = [
  {
    name: "Neo",
    age: 10,
    isVaild: true,
  },
  {
    name: "Lewis",
    age: 64,
    isVaild: false,
  },
  {
    name: "Evan",
    age: 123,
    isVaild: true,
  },
];

 

// type alias (타입 별칭)

type User = {
  name: string;
  age: number;
  isValid: boolean;
};

let userArr: User[] = [
  {
    name: "Neo",
    age: 10,
    isValid: true,
  },
  {
    name: "Lewis",
    age: 64,
    isValid: false,
  },
  {
    name: "Evan",
    age: 123,
   isValid: true,
  },
];

 

위의 `interface`와 `type alias`는 거의 같은데, 가장 큰 차이점이라면 `type`으로 선언된 타입은 재선언이 안되지만, `interface`는 재선언이 되며, 새롭게 만들어지기보다 기존 타입에 추가되는 방식이다. 아래 코드를 실행해보면 에러를 통해 확인해볼 수 있다.

interface Hello {
  name: string
}

interface Hello {
  age: number
} // Hello 는 name 과 age 모두를 포함하게 됨

type Hello2 = {
  name: string
}

type Hello2 = {
  age: number 
} // 불가능

 

구조적 타입 시스템과 타입 추론

TS는 구조적 타입 시스템으로, 실제로 어떻게 선언되었는지가 아니라, 어떤 property와 method를 가지고 있는지가 중요하다.

아래 예시에서 `p2`의 타입은 `Person`이라고 선언하지 않았지만, `p1`에 재할당할 수 있다.

interface Person {
    name: string;
    age: number;
}

let p1: Person = { name: "Alice", age: 25 };
let p2 = { name: "Bob", age: 30 };

p1 = p2; // 성공: p2는 Person과 동일한 구조를 가짐

 

만약 타입이 명시되어 있지 않다면 초기에 할당된 것을 통해 변수의 타입을 추론해 고정한다. (타입 추론 : type annotation)

let person = {
  name: "Alice",
  age: 25
}; // { name: string; age: number }로 추론

person.name = "Bob"; // 정상
person.age = 30; // 정상
person.age = "thirty"; // 오류: 'string' 형식은 'number' 형식에 할당할 수 없습니다.

 

타입 추론이 일어남에도 타입 추론에 의지하지 않는 이유는 TS의 특징인 안전한 코드 작성에서 멀어질 수 있기 때문이다.

 

 

 

내용이 길어져서 generic과 utility type에 대해서는 내일 작성하도록 하겠다.


잘 모르지만 c언어 같은 느낌을 받았다..ㅎ