DEV/WebProgramming

[TypeScript] TypeScript란, React에 적용하기

9thxg 2022. 7. 28. 23:28

기존 emotion-diary 프로젝트를 적용해보려 했지만 TypeScript가 익숙하지 않고 완성된 프로젝트에 적용하기엔 힘들다고 판단하여 새로운 프로젝트를 만들며 처음부터 적용해보려 한다.

 

목차
  0. TypeScript란
  1. TypeScript 적용
  2. TypeScript 사용하기

 


TypeScript란

 

// 예시 (코딩앙마 Typescript #1 영상 중)
// 기존 javascript 실행시
function add(num1, num2){
	console.log(num1 + num2);
}

add();	// NaN
add(1);	// NaN
add(1,2);	// 3
add(3,4,5);	// 7
add("hello", "world");	// "helloworld"

// typescript 적용
function add(num1: number, num2: number){
	console.log(num1 + num2);
}

add();	// err
add(1);	// err 
add(1,2);	// 3
add(3,4,5);	// err 
add("hello", "world");	// err

 

JavaScript 특성상 매우 자유롭기 때문에 함수를 만들거나 연산할 때 예상되는 타입이외에 타입을 사용한다해도 정상적으로 실행되는 경우가 많다. 이렇게 되면 다른 개발자가 함수를 사용할 때 예상한 실행결과와 다르게 나타날 경우 그 개발자는 직접 코드를 살펴보며 오류를 찾아야한다. 이는 JavaScript가 연산을 할 때 타입을 크게 따지지 않는 특성과 동적언어이기 때문에 런타임 즉 실행시간에 타입이 결정되고 오류가 발견되기 때문에 오류를 미연에 방지하기가 힘들다.

 

이와 달리 TypeScript는 정적언어이기 때문에 컴파일 단계에서 타입이 결정되기 때문에 맞지않은 문법이 존재한다면 오류를 확인하고 방지할 수 있다. 따라서 TypeScript를 사용하여 코드를 기술하여 가독성과 개발속도를 향상 시키고 이를 JavaScript로 변환하여 브라우저에서 실행하기 때문에 문제없이 실행이 가능하다.

 


TypeScript 적용

 

새로운 프로젝트를 시작할 때 적용한다면 CRA(create-react-app)을 통해 프로젝트를 생성할 때 --template를 typescript로 설정하여 실행해주면 함께 생성된다.

$ npx create-react-app app-name --template typescript

 

만약 기존의 프로젝트에 적용한다면 npm을 통해 typescript를 설치하면 된다.

$ npm install typescript @types/node @types/react @types/react-dom @types/jest

 

파일 확장자는 jsx형태라면 tsx로 변경해주고 js형태라면 ts로 변경해준다.

 

이후 react-router-dom이 필요하다면 react-router-dom과 typescript의 react-router-dom을 함께 설치하여 사용한다.

$ npm install react-router-dom
$ npm i @types/react-router-dom

 


 

TypeScript 사용하기

 

// 기본 타입
let car:string = 'kia';
let age:number = '26';
let isAdult:boolean = true;

let a:number[] = [1,2,3];
let b:Array<number> = [1,2,3];
let week:string[] = ['mon', 'tue', 'wed'];


// 튜플
let b:[string, number];
b = ['a', 1];
b = [2, 'b'; // err


// void: 아무것도 반환하지 않는 함수에 지정
function showLog():void{
    console.log();
}


// never: err만 반환하는 함수나 영원히 끝나지 않는 함수에 지정
function showError():never{
    throw new Error();
}

function infLoop():never{
    while(true){
    }
}


// enum: 비슷한 값들끼리 묶어둔 형태
// 자동으로 0부터 1씩 증가하는 값 부여
// 값을 지정해주면 이후 값은 지정한 값에 1씩 증가한 값 부여
enum Os {
    Window,	// 0
    Ios,	// 1
    Android	// 2
}

console.log(Os[1]); // Ios
console.log(Os['Android']);	// 2

// 문자열로 값을 지정한 경우 단방향 맵핑만 됨
enum Os{
    Window = 'win',
    Ios = 'ios',
    Android	= 'android'
}

let myOs:Os;
myOs = Os.Window;


// null, undefined
let a:null = null
let b:undefined = undefined

 

// 객체의 속성값의 타입을 정의해야 할 때 interface 사용
// 속성값이 선택적일 땐(Optional) 속성명 뒤에 ? 입력
// readonly를 속성명 앞에 지정하게 되면 첫 할당이후 읽기만 가능
interface User{
    name: string;
    age: number;
    gender?: string;
    readonly birthYear: number;
}

let user:User = {
    name = 'Jhon';
    age = 30;
    birthYear = 1993;
}

user.age = 10;
user.birthYear = 2012; // err


// 학년별 학점을 입력하는거와 같이 순차적인 인덱스에 값을 할당해야할 경우 문자열 인덱스 서명 추가
// 값이 고정된 선택지로 되어 있다면 type를 통해 선택지 지정 후 사용
type Score = 'A' | 'B' | 'C' | 'D' | 'F';

interface Student{
    1?: Score;
    2?: Score;
    3?: Score;
    4?: Score;
}

// 위와 같은 결과
interface Student{
    [grade:number]: Score;
}


// interface를 통한 함수 만들기
interface Add {
    (num1:number, num2:number):number;
}

const add:Add = function (x, y){
    return x+y;
}


// interface로 클래스 정의 - implement
interface Car{
    color: string;
    wheels: number;
    start(): void;
}

class Kia implement Car{
    color;
    wheels = 4;
    constructor(c: string){
        this.color = c;
    }
    start(){
        console.log('go...');
    }
}

const k = new Kia('blue');
k.start(); // 'go...'

// interface 확장 - extends
// 확장시 상속받은 속성을 모두 정의해야함
interface Benz extends Car{
    door: number;
    stop(): void;
}

const benz:Benz = {
    door = 4;
    stop(){
        console.log('stop');
    }
    color;
    wheels = 4;
    constructor(c: string){
        this.color = c;
    }
    start(){
        console.log('go...');
    }
}

// 확장은 여러개 동시에 가능
interface ToyCar extends Car, Toy{
    property1 : number;
    ...
}

 

// 함수
// 두 함수는 같은 결과를 나타냄
function hello(name? : string){
    return `Hello, ${name || "world"}`;
}

function hello(name = "world"){
    return `Hello, ${name}`;
}


// 선택적 매개변수는 매개변수 가장 앞으로 올 수 없음
function hello(name: string, age?: number): string{
    if (age !== undefined) {
        return `Hello, ${name}, You are ${age}.`;
    } else {
        return `Hello, ${name}`;
    }
}
console.log(hello("Tom", 30));

// 만약 앞으로 옮기고 싶다면 undefined를 명시적으로 매개변수로 전달해야함
function hello(age: number | undefined, name: string): string{
    if (age !== undefined) {
        return `Hello, ${name}, You are ${age}.`;
    } else {
        return `Hello, ${name}`;
    }
}
console.log(hello(30, "Tom"));
console.log(hello(undefined, "Tom"));


// 나머지 매개변수 작성법
function add(...nums) {
    return nums.reduce((result, num) => result + num, 0);
}
add(1,2,3);	// 6
add(1,2,3,4,5,6,7,8,9,10);	// 55


// this에 대한 정의는 매개변수 가장 앞에 지정
// 매개변수에 this에 대한 정의가 있지만 실제 함수 사용시 제외
interface User{
    name: string;
}

const Tom: User = {name: 'Tom'}

function showName(this:User, age:number, gender:'m'|'f'){
    console.log(this.name, age, gender)
}

const a = showName.bind(Tom);
a(30, 'm');


// 함수 오버로딩
// javascript 경우 동일한 매개변수라도 다른 타입이 가능함
// 매개변수 타입에 따라 결과값의 타입이 달라지는 경우 함수 오버로딩을 해주어야함
interface User{
    name: string;
    age: number;
}

function join(name: string, age: number): User;
function join(name: string, age: string): string;
function join(name: string, age: number | string): User | string{
    if(typeof age === "number"){
        return{
            name,
            age,
        };
    } else {
        return "나이는 숫자로 입력해주세요";
    }
}

const sam: User = join("Sam", 30);
const jane: User = join("Jane", "30");

 

// Literal Types 리터럴 타입
const userName1 = "Bob"; // userName1의 타입은 "Bob"임, 이를 문자열 리터럴 타입이라 함


// type로 enum과 비슷하게 만들 수 있음
type Job = "police" | "developer" | "teacher";

interface User{
    name: string;
    job: Job;
};

const user: User= {
    name: "Bob",
    job: "studnet" // err
    job: "developer"
}


// Union Types "|"
interface Car{
    name: "car";
    color: string;
    start(): void;
}

interface Mobile{
    name: "mobile";
    color: string;
    call(): void;
}

// 동일한 속성 타입을 통해 구별가능한 것을 식별가능한 유니온 타입이라고 함
function getGift(gift: Car | Mobile){
    console.log(gift.color);
    if (gift.name === "car"){
        gift.start();
    } else {
        gift.call();
    }
}


// Intersection Types "&"
// 교차타입으로 묶어줬다면 두 타입의 속성값을 모두 명시해주어야함
interface Car{
    name: string;
    start(): void;
}

interface Toy{
    name: string;
    color: string;
    price: number;
}

const toyCar: Toy & Car = {
    name: "타요",
    start(){},
    color: "red",
    price: 1000,
}

 

// Class 선언
// javascript와 다르게 typescript는 접근제한자가 존재
// public(default) : 자식 클래스, 클래스 인스턴스 모두 접근 가능
// protected : 자식 클래스에서 접근 가능
// private : 해당 클래스 내부에서만 접근 가능, 변수명 앞에 #붙이는 것과 동일

// static으로 선언된 속성은 클래스명으로 접근해야함(전역 사용 가능)
class Car{
    protected name: string = "car";
    #color: string; // private color: string;
    static wheels = 4;
    constructor(color: string){
        this.color = color;
    }
    start(){
        console.log("start");
        console.log(Car.wheels);
    }
}

class Bmw extends Car{
    constructor(color: string) {
        super(color); // color가 private이기 때문에 err
    }
    showName(){
        console.log(super.name); // protected이기 때문에 자식 클래스에서 사용가능
    }
}

const z4 = new Bmw("red");
console.log(z4.name); // protected이기 때문에 err
console.log(Car.wheels);


// 추상클래스
// 추상클래스는 new로 할당 불가능, 오직 상속을 통해서만 사용가능(extends)
// 추상메소드는 반드시 상속받은 클래스에서 구현해주어야함
abstract class Car{
    color: string;
    constructor(color: string){
        this.color = color;
    }
    start(){
        console.log("start");
    }
    abstract doSomething():void;
}

const car = new Car("blue"); // err 

class Bmw extends Car{
    constructor(color:string){
        super(color);
    }
    doSomething(){
        alert(3);
    }
}

 

// Generic
// 클래스, 함수, 인터페이스를 다양한 타입으로 재사용 가능
// 타입파라미터 사용, 주로 T로 사용
// 전달되는 매개변수에 따라 타입추론이 되어서 사용할 때 타입정의 생략가능
function getSize<T>(arr: T[]):number{
    return arr.length;
}

const arr1 = [1,2,3];
getSize<number>(arr1); // 3

const arr2 = ['a', 'b', 'c'];
getSize<string>(arr2); // 3

const arr3 = [true, false, false];
getSize<boolean>(arr3); // 3

const arr4 = [{}, {name: "tom"}, {}];
getSize<object>(arr4); // 3

// 프로퍼티에 어떤 데이터가 들어올지 모를때
interface Mobile<T>{
    name: string;
    price: number;
    option: T;
}

const m1: Mobile<{color: string; coupon: boolean;}> = {
    name: "s21",
    price: 1000,
    option: {
        color: "yellow",
        coupon: false,
    },
};

const m2: Mobile<string> = {
    name: "s20",
    price: 900,
    option: "good",
};

// 모든 매개변수에 특정 프로퍼티가 존재한다는 걸 장담할 수 없을때
// T타입에서 name 속성을 string으로 확장한 형태로 지정
// name이 string이 아니거나 없다면 오류 발생
interface User{
    name: string;
    age: number;
}

interface Car{
    name: string;
    color: string;
}

interface Book{
    price: number;
}

const user: User = {name: "a", age: 10};
const car: Car = {name: "bmw", color: "blue"};
const book: Book = {price: 1000};

function showName<T extends {name: string}>(data: T): string{
    return data.name;
}

showName(user);
showName(car);
showName(book);	// err name속성이 없기때문

 

// 유틸리티 타입

// keyof, 속성명을 유니온타입으로 가져옴
interface User{
    id: number;
    name: string;
    age: number;
    gender: "m" | "f";
}

type UserKey = keyof User; // 'id' | 'name' | 'age' | 'gender' 와 같음


// Partial<T>, 해당 인터페이스 부분만 구현가능, 단 새로운 속성은 정의하지 못함
let admin: Partical<User> = {
    id: 1,
    name: "Tom",
};


// Required<T>, Partial과 반대로 속성 모두 구현해야함
let admin: Required<User> = {
    id: 1,
    name: "Tom",
    age: 30,
    gender: 'm',
};


// Readonly<T>, 첫 할당 이후 읽기만 가능
let admin: Readonly<User> = {
    id: 1,
    name: "Tom",
};

admin.id = 3; // err


// Record<K, T>, K값인 키값에 T값의 타입값을 맵핑
type Grade = '1'|'2'|'3'|'4';
type Score = 'A'|'B'|'C'|'D'|'F';

const score: Record<Grade, Score> = {	// Record<'1'|'2'|'3'|'4', 'A'|'B'|'C'|'D'|'F'>
    1: "A",
    2: "C",
    3: "B",
    4: "D",
}

interface User{
    id: number;
    name: string;
    age: number;
}

function isValid(user: User{
    const result: Record<keyof User, boolean> = {
        id: user.id > 0,
        name: user.name !== "",
        age: user.age > 0,
    };
    return result;
}


// Pick<T, K>, T타입에서 K프로퍼티만 불러와 사용
interface User{
    id: number;
    name: string;
    age: number;
    gender: "m" | "f";
}

const admin: Pick<User, "id" | "name"> = {
    id: 0,
    name: "Tom",
};


// Omit<T, K>, Pick과 반대로 K프로퍼티를 제외한 프로퍼티 사용
const admin: Omit<User, "age" | "gender"> = {
    id: 0,
    name: "Tom",
};


// Exclude<T1, T2>, T1타입에서 T2타입과 겹치는 타입을 제거
type T1 = string | number | boolean;
type T2 = Exclude<T1, number | string>; // boolean


// NanNullable<Type>, 타입중에서 null과 undefined 타입을 제거
type T1 = string | null | undefined | void;
type T2 = NanNullable<T1> // string | void