Typescript와 React - #1 타입스크립트 문법

sham·2021년 11월 24일
1

Betti 개발일지

목록 보기
7/14
post-thumbnail

타입스크립트 자료 정리 : https://codingmoondoll.tistory.com/entry/츄라이-츄라이-리액트-타입스크립트부제-나만-고통-받을-수는-없지

https://typescript-kr.github.io/pages/tutorials/ts-for-the-new-programmer.html
https://velog.io/@velopert/create-typescript-react-component
https://react.vlpt.us/using-typescript/
https://velog.io/@swimme/React-Typescript-시작하기

타입스크립트란?

자바스크립트 + 알파(확장). 자바스크립트의 모든 기능에 타입이라는 새로운 개념이 추가된다.
기존의 자바스크립트 코드에서는 자료형을 지정하지 않고 유연하고 자유로운 조작이 가능했었다. 그러나 프로젝트의 규모가 커지면 이 자유로움이라는 부분은 장점보다는 단점이 될 가능성이 크다.

타입스크립트의 특징으로는

  1. 컴파일 필요
  2. 타입 지정 필요
  3. 엄격한 검사

가 있다.

타입스크립트 컴파일

타입스크립트 파일을 html이 인식하지 못하므로, js로 변환해줄 필요가 있다.

타입 지정

타입 스크립트의 가장 큰 특징으로, 자료형이 존재하지 않는 자바스크립트에 자료형을 부여하여 준다.

엄격한 검사

console.log("" == ture) // false
console.log(0 == ture) // false
console.log("" == 0) // true????
console.log(1 < 3 < 2) // ture?????

이것이 자바스크립트다! 절망편

https://velog.io/@cada/자바스크립트에서-0이-true인-이유

단순히 위의 사례 말고도, 에러가 나도 제대로 잡지 않는 등 자바스크립트의 허술한 부분에 감탄한 사람들이 적지 않을 것이다. 타입스크립트에서는 어디에서 어떤 부분이 에러가 났는지 확실하게 잡아주게 된다.

타입스크립트 환경 세팅하기

https://www.kenrhee.com/blog/getting-started-with-typescript-with-react

mkdir typeScriptTest
cd typeScriptTest
npm install -g typescript
tsc --init

tsconfig.json 파일에는 타입스크립트가 컴파일 될 때 필요한 옵션들을 지정할 수 있다.

옵션들에 대한 자세한 설명은 다음 링크 참조.

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist"
  }
}

target - 컴파일된 코드가 어떤 환경에서 실행될 지 정의한다. 화살표 함수를 사용하는데 target이 es5로 되어 있다면 일반 함수로 자동 변환된다.

module - 컴파일된 코드가 어떤 모듈 시스템을 사용할 지 정의한다. es2015라면 아무런 변화가 없지만 commonjs 라면 export default Temp 를 할 때 exports.default = Temp로 변환될 것이다.

strict : 모든 타입 체킹 옵션을 활성화 한다.

esModuleInterop : commonjs 모듈 형태로 이루어진 파일을 es2015 모듈 형태로 불러올 수 있게 해준다.

outDir : ts 파일에서 js 파일로 컴파일된 파일들이 저장될 경로를 지정할 수 있다.

"strict" : true만 켜줘도 입문자들은 타입스크립트의 대부분의 기능을 사용할 수 있다.

타입스크립트 컴파일

// src/test.ts
let test : string = "1";
test = "42";

다음과 같은 타입스크립트는 실행하기 위해서 자바스크립트로 컴파일해서 실행해야만 한다.

타입스크립트를 컴파일할 때는 tsc {파일명} 형식을 이용한다.

tsc src/test.ts을 실행하면 src/test.js 파일이 생성된다.

//src/test.js
var test = "1";
test = "42";

tsc {파일명} -w 옵션을 넣어주면 터미널을 종료하기 전까지 실시간으로 파일의 변경을 감지해서 컴파일한다.

지금까지 글로벌로 설치한 타입스크립트 CLI로 코드를 컴파일 했는데, 프로젝트에서는 typescript를 설치해서 컴파일하게 된다.

기초 문법

변수(상수)의 타입 지정

let num : number;
num = 1;
num = "1"; // Type 'string' is not assignable to type 'number'

let str : string;
str = "123";
str = 123; // Type 'string' is not assignable to type 'number'

let numOrStr : string | number;
numOrStr = 1;
numOrStr = "1";
numOrStr = true; // Type 'boolean' is not assignable to type 'string | number'.

let list: number[];
list = [1, 2, 3];
list = [1, 2, "3"]; // Type 'number' is not assignable to type 'string'.
let anotherList: Array<number> = [1, 2, 3];

let tuple : [string, number];
tuple = ["hello", 1];
tuple = [1, "hello"]; // Type 'string' is not assignable to type 'number'.

let object : {name : string};
object = {name : "123"};
object = {name : 123}; // Type 'string' is not assignable to type 'number'.

type CusType = string | number;
let cum : CusType;

타입스크립트에서 모든 변수나 상수에 타입을 지정해줄 수 있다. 이 때 타입에 맞지 않는 값을 집어넣을 경우 IDE 상에서도, 컴파일 시에도 어떤 부분이 잘못되었는지를 상세한 에러를 뿜어준다.

중요할 것 같은 타입이나 문법에 대해 추가로 기술하자면

배열 타입 지정

배열의 타입을 지정하고 싶다면 타입[] 형태로 지정한다. 제네릭 배열 타입을 이용해서 Array<타입> 형태도 가능하다. 해당 배열 안의 모든 요소는 명시한 타입이어야만 한다.

튜플 타입 지정

배열의 요소의 타입, 개수를 정확하게 설정하고 싶다면 [타입, 타입...] 형태로 지정할 수 있다.

객체 타입 지정

객체의 키에 해당하는 값에 타입을 객체 : { 키 : 타입 } 형태로 지정한다.

여러 타입 중 하나

|를 사용하면 복수의 타입을 설정할 수 있다. 설정한 타입 중 하나도 속하지 않는 값일 경우 에러가 발생한다.

함수의 타입 지정

const sum = (x : number, y : number) : number => {
    return x + y;
}
const wrongSum = (x : number, y : number) : number => {
    return "x + y"; // Type 'string' is not assignable to type 'number'.
}

const useless = () : void => {
    console.log("나는 쓰레기야...");
}

console.log(sum(1, 2));

console.log(sum(1, "2")); // Argument of type 'string' is not assignable to parameter of type 'number'.

함수의 인자 하나하나 마다, 리턴하는 값까지 타입을 지정해줄 수 있다.

리턴을 하지 않는 함수의 경우 리턴 부분의 타입을 void로 설정해주자. 안 해줘도 에러는 나지 않는다.

인터페이스

클래스나 객체를 위한 타입을 지정할 때 사용하는 문법.

클래스 인터페이스

클래스(생성자 함수)에 대해 헷갈리다면 다음 링크로.
https://ordinary-code.tistory.com/22
https://webclub.tistory.com/136

클래스가 특정 조건을 준수하게끔 하고 싶을 때, interface를 설정해서 갖추어야 할 요구사항을 설정할 수 있다.

클래스를 선언할 때 implements 키워드를 사용하여 특정 interface 의 요구사항을 충족하도록 명시 할 수 있다. interface 의 요구사항과 다르다면 에러를 발생한다.

// Human이라는 인터페이스를 생성한다. 이 안에는 이 인터페이스가 갖추어야 할 조건이 들어간다.
interface Human {
    weight : number,
    age : number,
    getBmi() : number 
}

class Minsu implements Human { // Minsu라는 클래스는 Human의 조건을 충족하겠다는 의미.
    weight : number;
    age : number;
    nickname : string;
    constructor(weight : number, age : number, nickname : string) {
        this.weight = weight;
        this.age = age;
        this.nickname = nickname;
    }
    getBmi() {
        return this.weight / this.age;
    }
}
const minsu: Minsu = new Minsu(67, 15, "Min");
console.log(minsu);
console.log(minsu.getBmi());

Human이라는 인터페이스의 조건은 number 타입의 weightage, number를 리턴하는 getBmi가 있어야 하는 것이다.

Minsu 클래스를 선언하며 implementsHuman 클래스의 조건을 충족할 것임을 명시한다. interface와 동일한 형태의 멤버 변수를 설정하고 생성자 쪽에서 인자로 들어온 값들을 설정해준다.

Minsu 클래스에 멤버 변수의 타입과 맞는 인자를 전달해서 Minsu1이라는 인스턴스를 생성한다.

accessor

이 때 constructor 의 파라미터 쪽에 public 또는 private accessor 를 사용하면 직접 하나하나 설정해주지 않아도 된다.

// Human이라는 인터페이스를 생성한다. 이 안에는 이 인터페이스가 갖추어야 할 조건이 들어간다.
interface Human {
    weight : number,
    age : number,
    getBmi() : number 
}

class Minsu implements Human { // Minsu라는 클래스는 Human의 조건을 충족하겠다는 의미.
    constructor(public weight : number, public age : number, public nickname : string) {
        this.weight = weight;
        this.age = age;
        this.nickname = nickname;
    }
    getBmi() {
        return this.weight / this.age;
    }
}
class Minji implements Human {
    constructor(public weight : number, private age : number, private nickname : string) {
        this.weight = weight;
        this.age = age; // Class 'Minji' incorrectly implements interface 'Human'.
        this.nickname = nickname;
    }
    getBmi() {
        return this.weight / this.age;
    }
}

const minsu: Minsu = new Minsu(67, 15, "Min");
const minji: Minji = new Minji(44, 11, "Ji");
console.log(minsu.nickname);
console.log(minji.nickname); // Property 'nickname' is private and only accessible within class 'Minji'.

public accessor은 특정 값이 클래스의 코드 밖에서도 조회 가능함을 의미한다.

private accessor는 반대로 클래스 밖에서 조회할 수 없음을 의미한다. 만약 private 로 설정한 값이 interface 에서 요구한 조건이었다면 에러로 인식한다. interface 의 조건이 되는 멤버는 public으로 돌리자.

객체 인터페이스

interface Person {
    name: string;
    age?: number; // 물음표가 들어갔다는 것은, 설정을 해도 되고 안해도 되는 값이라는 것을 의미한다.
  }
  interface Developer {
    name: string;
    age?: number;
    skills: string[];
  }
  
  const person: Person = {
    name: 'sham',
    age: 20
  };
  
const expert: Developer = {
    name: 'sham',
    skills: ['javascript', 'react'],
    useless : "yeah!" // Type '{ name: string; skills: string[]; useless: string; }' is not assignable to type 'Developer'.
  };

객체의 타입을 interface 로 지정한 후 객체가 해당 인터페이스의 형태에서 벗어나게 되면 에러가 발생한다.

인터페이스에 없는 키가 들어간 경우에도 에러로 인식한다.

다만 키 뒤에 ? 가 붙으면 해당 키는 있어도 되고 없어도 되는 값으로 인식한다.

interface 상속(extends)

interface Person {
    name: string;
    age?: number; // 물음표가 들어갔다는 것은, 설정을 해도 되고 안해도 되는 값이라는 것을 의미한다.
  }
  interface Developer extends Person{
    skills: string[];
  }
  
  const person: Person = {
    name: 'sham',
    age: 20
  };
  
  const expert: Developer = {
    name: 'sham',
    skills: ['javascript', 'react'],
  };

Type Alias

type 키워드를 이용하면 나만의 커스텀 타입을 만들 수 있는데, 이를 이용해 특정 객체, 배열을 위한 타입을 만들 수 있다.

type Person = {
    name: string;
    age?: number; 
  };
  
  type Developer = Person & {  // & 는 Intersection 으로서 두개 이상의 타입들을 합쳐준다.
    skills: string[];
  };
  
  const person: Person = {
    name: 'sham'
  };
  
  const expert: Developer = {
    name: 'sham',
    skills: ['javascript', 'react']
  };
  type People = Person[]; // Person[] 를 이제 앞으로 People 이라는 타입으로 사용 할 수 있다.
  const people: People = [person, expert];

  type Color = 'red' | 'orange' | 'yellow';
  const color: Color = 'red';
  const colors: Color[] = ['red', 'orange'];
  type Password = 1 | 4 | 6 | 7;
  const tryPass : Password[] = [1, 4, 5, 7]; // Type '5' is not assignable to type 'Password'.

Intersection(&)

&를 이용하면 두 개 이상의 타입들을 합쳐 줄 수 있다.

People이라는 타입은 Person이라는 타입을 요소로 하는 배열을 의미한다.

people의 타입을 People로 선언하고 그 요소로 person과 expert를 넣어준다. expert는 Person 타입을 충족하기에 정상 작동된다.

자유로운 타입 지정

자료형만 타입으로 지정할 수 있는 것이 아니다.

문자열, 숫자 또한 타입으로 지정해줄 수도 있다!

타입과 인터페이스의 차이

https://rinae.dev/posts/practical-advanced-typescript-summary#switch-문에서-자동으로-타입-추론하기

두 가지 모두 무언가 구조를 정의할 때 사용할 수 있다. 서로 다른 타입과 결합도 가능하다. (interface - extends / type - &).

두 형태의 타입 정의 방식을 교차해서 사용할 수도 있다. type이 interface나 다른 type과 함께 결합될 수도 있고, 클래스에 extendsimplements 를 쓸 때 interface뿐 아니라 type도 가져올 수 있다. 하지만 유니언 타입은 extendsimplements 에 사용될 수 없다.

type은 같은 파일 안에서 두 번 선언 될 수 없지만, interface는 중복 선언될 경우 타입 결합과 동일하게 동작한다. 이 원리를 활용하여 라이브러리의 타입을 확장하는데도 사용할 수 있다.

까마득한 먼 미래에 라이브러리 작성하게 된다면 공개되는 타입 형태를 interface로 내보내어 사용자들이 필요할 경우 확장하기 쉽게 만들 수 있을 것이다.

제네릭(Generics)

제네릭(Generics)은 타입스크립트에서 함수, 클래스, interfacetype을 사용하게 될 때 여러 종류의 타입에 대하여 호환을 맞춰야 하는 상황에서 사용하는 문법이다.

type Arr = {
    [key : string] : string;
}
type Num = {
    [key : string] : number;
}

function merge(a: any, b: any): any {
    return {
      ...a,
      ...b
    };
  }
  
  const merged = merge({ name: "1" }, { index: 1 });
  console.log(merged);

merge 함수는 인자로 들어온 객체를 서로 합쳐주는 함수이다. 그러나 인자로 어떤 타입이 들어올지 모르는 상황이다. any 라는 타입을 쓸 수도 있겠지만 결국 인자로 들어온 객체나 합쳐서 나가는 객체에 무엇이 들었는지 알 수 없는 상황, 타입추론을 하는 의미가 없어진다.

이때 제네릭을 사용하면 알 수 없는 타입이 인자로 들어와도 타입을 그대로 보장한 채 사용해줄 수 있다.

function merge<A, B>(a: A, b: B): A & B {
  return {
    ...a,
    ...b
  };
}

const merged = merge({ foo: 1 }, { bar: 1 });

꺽쇠 안에 타입의 이름을 넣음으로써 그 어떤 타입이 들어와도 타입을 보장해줄 수 있다.

인자로 들어온 a는 A라는 타입으로 인식하고 b는 B라는 타입으로 인식하고 A와 B를 더한 타입을 리턴한다. 어떤 타입인지 함수에서 알 수 없지만 타입을 지켜내는데 성공한 것이다.

  function same<T>(param: T) {
  return param;
}

console.log(same("hello!"));

함수의 인자, 리턴 값으로 다양한 타입이 들어오게 될 때 제네릭을 사용하면 타입 지원을 지켜낼 수 있다.

interface에서 제네릭 사용

interface Items<T> {
  list: T[];
}

type TypeItems<T> = {
  list: T[];
}

const stringItem: Items<string> = {
  list: ["1, 2, 3"]
};

const numberItem: Items<number> = {
  list: [1, 2, 3],
};
const strnumItem: Items<number | string> = {
  list: [1, 2, "3"]
};

인터페이스에서도 제너릭을 지정해주어서 해당 인터페이스를 실제로 사용하게 될 때 타입을 지정하게끔 할 수도 있다. type의 사용법과 거의 같기에 같이 묶어서 넘어가겠다.

클래스에서 제네릭 사용

class Queue<T> {
  list: T[] = [];
  length() {
    return this.list.length;
  }
  enqueue(item: T) {
    this.list.push(item);
  }
  dequeue() {
    return this.list.shift();
  }
}

const queue = new Queue<number | string>();
queue.enqueue(0);
queue.enqueue("1");
queue.enqueue(2);
queue.enqueue("3");
queue.enqueue(4);
console.log(queue.dequeue());
console.log(queue.dequeue());
console.log(queue.dequeue());
console.log(queue.dequeue());
console.log(queue.dequeue());

클래스에 사용하게 되는 제네릭은/ 클래스 자체의 타입이 아닌/ 클래스 내부에서 사용되게 될 타입이/ 클래스를 선언하면서 결정된다는 것을 말한다.

profile
씨앗 개발자

0개의 댓글