TypeScript로 객체지향 프로그래밍 해보기!
TS로 class를 어떻게 만들까!?
class Player {
// JS의 constructor 안에 this.name = name 같은 코드를 안 써도
// 파라미터를 써주기만 하면, TS가 알아서 Constructor 함수를 만들어 줌
constructor(
private firstName: string,
private lastName: string,
) {}
}
---
// 다음과 같은 JS 코드가 만들어짐
"use strict";
class Player {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
거의 모든 객체지향 프로그래밍 언어들이 가지고 있는 특징인 private 혹은 public property를 만들 수 있음. 하지만 private 부분은 컴파일되면서 사라져서 JS에서 보이지 않음. 이 키워드는 오로지 TS가 우릴 보호해 주기 위해서만 사용하며 JS에선 사용되지 않음.
class Player {
constructor(
private firstName: string,
private lastName: string,
public nickname: string
) {}
}
const nico = new Player('nico', 'las', '니꼬')
nico.firstName // firstName은 private이라 작동 x
// 다시 한번 말하지만, JavaScript에서는 아무 문제 없이 작동함
// 내 프로젝트에서는 이런 식으로 코드를 쓰면 컴파일되지 않음
// 나에게 있는 유일한 옵션은 nickname
TypeScript와 객체지향 프로그램이 가지고 있는 엄청 훌륭한 것은 추상 클래스(Abstract Class)임. 추상클래스는 다른 클래스가 상속받을 수 있는 클래스로, 이 클래스는 직접 새로운 인스턴스를 만들 수는 없음. 다시 한번 설명하면 추상 클래스는 오직 다른 곳에서 상속받을 수만 있는 클래스로 직접적으로 인스턴스를 만들지 못함!
abstract class User {
constructor(
private firstName: string,
private lastName: string,
public nickname: string
) {}
}
class Player extends User {
}
// const nico = new User // x
const nico = new Player('nico', 'las', '니꼬')
User로부터 getFullName 이 메소드를 상속받았으므로 Player는 이걸 그냥 쓸 수 있음
abstract class User {
constructor(
private firstName: string,
private lastName: string,
public nickname: string
) {}
getFullName() {
return `${this.firstName} ${this.lastName}`
}
}
class Player extends User {
}
const nico = new Player('nico', 'las', '니꼬')
nico.getFullName()
다시 한번 말하지만, 컴파일된 JS를 살펴보면 abstract가 전혀 언급되지 않음 그냥 일반적인 클래스임. 그리고 만약 getFullName method에 private를 작성해도 문제없이 작동함. getFullName은 추상 클래스 안에 들어가 있는 메소드인데 더 놀라운 거은 우리가 추상 메소드를 만들 수 있다는 거임
추상 메소드를 만들려면, 메소드를 클래스 안에서 구현하지 않으면 됨. getFullName() {...}
이 부분이 메소드의 implementation(구현)으로 메소드는 클래스 안에 존재하는 함수임. 추상 클래스 안에서는 추상 메소드를 만들 수 있음! 하지만, 메소드를 구현해서는 안 되고 대신 call signature만 적어둬야 함
예를 들어, User 추상 클래스 안에 getNickName 이라는 추상 메소드가 있다고 하자. 추상 메소드는 우리가 추상 클래스를 상속받는 모든 것들이 구현을 해야 하는 메소드를 의미함
abstract class User {
constructor(
private firstName: string,
private lastName: string,
private nickname: string
) {}
// 이건 메소드의 call signature 만을 가지고 있음
abstract getNickName(): void
getFullName() {
return `${this.firstName} ${this.lastName}`
}
}
// TS가 Player는 getNickName을 구현해야 한다고 "빨간 줄"로 친절히 알려줌
class Player extends User {
getNickName() {
// 문제는 여기서 console.log를 사용하지 못함.
// 왜냐면 private property로 만들었기 때문
// property를 private으로 만든다면 그 클래스를 상속했을지라도 접근할 수 없음
console.log(this.nick.. 접근 x)
}
}
const nico = new Player('nico', 'las', '니꼬')
nico.getFullName()
필드를 보호하기 위한 방법은 하나가 더 있음. private property 들은 인스턴스 밖에서 접근할 수 없고 다른 자식 클래스에서도 접근할 수 없음. 말 그대로 개인적인 것을 말하고 User class의 인스턴스나 메소드에서 접근할 수 있으나 추상 클래스여서 인스턴스화를 할 수 없음. 필드가 외부로부터는 보호되지만 다른 자식 클래스에서 사용되기를 원한다면 protected를 씀. 이건 클래스 밖에선 접근할 수 없지만 User를 상속하면 User.nickname에 접근할 수 있음
abstract class User {
constructor(
protected firstName: string,
private lastName: string,
private nickname: string
) {}
abstract getNickName(): void
getFullName() {
return `${this.firstName} ${this.lastName}`
}
}
class Player extends User {
getNickName() {
console.log(this.nickname)
}
}
const nico = new Player('nico', 'las', '니꼬')
nico.getFullName()
TS에서는 필드가 어떠한 보호 등급인지(접근 제어자), 이름 그리고 타입을 써주면 됐음. 그러면 남은 것들을 해줌~ 뭐가 오든 JS에서는 보이지 않는다는 것이 중요함. 그리고 우린 TS가 추상 클래스를 쓸 수 있도록 해주는 걸 알았음 직접적으로 인스턴스를 만들진 못하지만 상속할 수 있음.
추상 메소드는 구현이 되어 있지 않은 (코드가 없는) 메소드. call signature만 가지고 있는데 argument를 받을 경우 이름과 타입 그리고 함수의 리턴 타입을 정의하면 되고, 추성 메소드가 있는 경우 추상 클래스를 상속받는 클래스에선 추상 메소드를 구현해 줘야 함
기본적으로 모든 것은 public으로, nickname을 private로 바꾸면 User 클래스 안에서만 접근이 가능. nickname을 User 클래스를 상속하는 모든 클래스에서 사용 가능하도록 만들고 싶다면 protected로 바꿔줘야 함. 모든 보호 기능은 TS에서 작동!
abstract class User {
constructor(
private firstName: string,
private lastName: string,
private nickname: string
) {}
abstract getNickName(): void
getFullName() {
// this.nickname
return `${this.firstName} ${this.lastName}`
}
}
class Player extends User {
getNickName() {
console.log(this.nickname)
}
}
const nico = new Player('nico', 'las', '니꼬')
nico.getFullName()
해싱 알고리즘을 쓰는 해시맵 사전?!
✅ Typescript 코드
https://github.com/LIMON029/NomadTypescript/blob/main/src/Nomad%234.ts
✅ 컴파일된 js 파일
https://github.com/LIMON029/NomadTypescript/blob/main/out/Nomad%234.js
// 이 object는 제한된 양의 property 만을 가질 수 있음
// property 의 이름은 모르지만, 타입만 알고 있을 때 씀
type Words = {
[key: string]: string
}
class Dict {
// constructor에서 직접 초기화되지 않는 property
private words: Words
constructor() {
// 수동 초기화
this.words = {}
}
// 클래스를 타입처럼 사용할 수 있음
// 이 파라미터가 이 클래스의 인스턴스이기를 원하면!
add(word: Word) {
if(this.words[word.getTerm()] === undefined) {
this.words[word.getTerm()] = word.getDef()
} else {
console.log(`This term(${word.getTerm()}) is already exist.`)
}
}
def(term: string) {
return this.words[term]
}
del(word: string | Word) {
let term: string
if(typeof(word) === "string") {
term = word
} else {
term = word.getTerm()
}
if(this.words[term] !== undefined) {
delete this.words[term]
} else {
console.log(`This term(${term}) is not exist.`)
}
}
update(arg1, arg2?:string) {
let term: string, def: string
if(arg2) {
term = arg1
def = arg2
} else {
term = arg1.getTerm()
def = arg1.getDef()
}
this.words[term] = def
}
print() {
console.log(this.words)
}
}
class Word {
constructor(
// 값을 보여주고는 싶지만 수정은 불가능하게
public readonly term: string,
public readonly def: string
// kimchi.def = 'xxx; (작동 X - 데이터 덮어쓰기 방지)
) {}
getTerm = () => this.term
getDef = () => this.def
addDef(extraDef: string) {
this.def = `${this.def}, ${extraDef}`
}
updateDef(newDef: string) {
this.def = newDef
}
print() {
console.log(`${this.term}: ${this.def}`)
}
}
const dict = new Dict()
const kimchi = new Word("kimchi", "김치")
const cider = new Word("cider", "사이다")
dict.add(kimchi)
dict.add(cider)
kimchi.updateDef("한국의 음식, 김치")
kimchi.print()
cider.addDef("탄산음료")
cider.print()
dict.update(kimchi)
dict.update(cider)
dict.del("cider")
dict.del(kimchi)
dict.print()
const light = new Word("light", "빛")
dict.add(light)
dict.print()
인터페이스는 우리에게 익숙한 타입과 비슷하지만, 두 가지 부분에서 다른 점을 이해해야 함. 우선 TS에서 type을 사용하는 게 얼마나 유용한 것인지 기억! 타입 alias(대체명) 를 쓸 수도 있고 우리가 원하는 모든 것들의 타입을 만들 수 있음
// type alias
type Nickname = string
type Health = number
type Freiends = Array<string>
type Player = {
nickname: Nickname,
healthBar: Health
}
const nico: Player = {
nickname: 'niko',
healthBar: 10
}
type Food = string;
const icecream: Food = 'sweet'
타입을 지정된 옵션으로만 제한할 수도 있음. Type 은 매우 다재다능한 키워드
// 이런 식으로 concrete 타입의 특정 값
type Team = 'red' | 'blue' | 'yellow'
type Player = {
nickname: string,
// 일반적인 string 전체가 아닌 특정 string
team: Team
}
const nico: Player = {
nickname: 'niko',
team: 'red' // 'green' - error
}
이제 오브젝트의 모양을 설명하는 다른 방법인 인터페이스를 알아보자.
코드를 구현하는 데에선 아무것도 변화가 없고 모든 것이 똑같이 동작함. 가장 먼저 이해해야 할 것은, 타입은 우리가 원하는 모든 것이 될 수 있음. (red|blue|yellow)
하지만 인터페이스는 오직 한 가지 용도만을 가지고 있음 그리고 그건 오브젝트의 모양을 특정해 주기 위한 것으로, 이건 React.js를 이용해 작업을 할 때 많이 사용함!
타입스크립트에게 오브젝트의 모양을 알려주는 방법엔 두 가지가 있음. type = {} & interface 이 둘은 오브젝트의 모양을 결정한다는 같은 역할을 함. 하지만 다른 점은, type 키워드는 interface 키워드에 비해 좀 더 활용할 수 있는 게 많음
type Team = 'red' | 'blue' | 'yellow'
interface Player = {
nickname: string,
// 일반적인 string 전체가 아닌 특정 string
team: Team
}
const nico: Player = {
nickname: 'niko',
team: 'red' // 'green' - error
}
인터페이스로는 이런 걸 할 수 없음. interface는 오로지 오브젝트의 모양을 타입스크립트에게 설명해 주기 위해서만 사용되는 키워드로 오로지 이 한 가지 목적만을 가짐. type 키워드는 다양한 목적으로 사용될 수 있음 오브젝트의 모양을 정의, 특정 값들로만 제한, 타입 alias 만들기 등 원하는 모든 걸 할 수 있음
interface Hello = string // 작동 x
type Person = { ... }
// interface가 좀 더 짧음
interface Person { ... }
네가 인터페이스를 쓰는 사람들을 본다면 그 사람들은 그저 오브젝트의 모양을 타입스크립트에게 설명해 주고 싶을 뿐! 둘 다 같은 역할을 하니까 둘 중 하나를 써도 상관없음. 이 둘은 모두 타입스크립트에게 오브젝트의 모양을 설명하고 있음
문법에서의 차이점 말고도, type이 좀 더 많이 쓰임. 인터페이스를 다루는 게 클래스를 다루는 듯한 느낌이라 더 쉬울 수 있음. 인터페이스는 클래스와 닮았음
interface User {
name: string // readonly name: string
}
interface Player extends User {}
const nico: Player = {
name: 'nico'
}
// 타입이라면? 똑같이 동작하지만 조금 다르게 생김
type User = { name: string }
type Player = User & {} // and 연산자를 사용
const nico: Player = { name: 'nico' }
우리는 인터페이스를 타입스크립트에게 오브젝트의 모양을 설명할 때만 사용할 수 있음. 이것이 인터페이스를 쓸 수 있는 유일한 때. type은 종류에 관계없이 어떠한 타입을 만들 때 다 쓸 수 있으므로 많이 다름
원한다면 오브젝트의 모양을 타입을 이용하여 정의할 수 있지만 타입스크립트에게 오브젝트의 모양을 알려줄 때는 인터페이스 사용을 권장함. 이것의 문법 구조가 객체지향 프로그래밍처럼 보여서 이해하기 쉬울 수 있기 때문임
인터페이스의 또 다른 특징으로는 property 들을 축적시킬 수 있다는 거임. 조금 더 나은 합체능력을 가지는데 type으로는 할 수 없음
// 인터페이스 3개를 타입스크립트가 알아서 하나로 합쳐줌
type User = { name: string }
interface User { lastName: string }
interface User { health: number }
const nico: User = {
name: 'nico',
lastName: 'las',
health: 10
}
단순히 타입스크립트에게 오브젝트의 모양을 알려주고 싶다면 타입이나 인터페이스 중에서 아무거나 써도 좋음. 하지만 인터페이스는 객체 지향 프로그래밍의 개념을 활용해서 디자인되었고, 타입은 더 유연함 타입 alias를 만들 수도 있고 지정된 값들만 가지도록 타입으로 제한을 할 수도 있는 등 조금 더 개방적이라 말할 수 있음.
기능상 큰 차이가 없다고 할 수 있지만, type을 정의하기 위한 용도/class의 형태를 정의하기 위한 용도로 나뉘는게 맞는 것 같습니다. 같은 동작이 이뤄지더라도 type을 사용한 경우 자료형을 만든 것, interface를 사용한 경우 객체의 명세를 작성한 것으로 보는 것이 바람직해 보입니다
OOP 를 위한 Typing 은 class 확장을 위해 필수적으로 interface 를 써야 할 것이고,
Javascript 의 유동적인 Ducktype 을 고정타입
으로 선언하기 위해서
TS 고유의 type
키워드를 사용해야 된다고 생각합니다.
interface 와 type 은 같은 목적으로, 타입스크립트에게 오브젝트의 모양을 알려줄 수 있음. 하지만 완전 같지는 않고 다른 점들이 있는데 서로 상속하는 방법과 문법이 다르고 interface는 다른 property 들을 합칠 수 있음
지난 강의에서 배운 추상 클래스는 유용한데, 다른 클래스가 가져야 할 property 랑 메소드를 명시할 수 있도록 도와줌. 다시! 추상 클래스는 이걸 상속받는 다른 클래스가 가질 property 와 메소드를 지정하도록 해줌. 상속받는 클래스가 어떻게 동작해야 할지 "무엇을 구현해야 할지" 일러주기 위해서 추상 클래스를 사용함
문제점은 자바스크립트에는 abstract 개념이 없어서 컴파일 시 일반적인 클래스로 변함. 그러면 왜 추상 클래스를 만들까? 우린 다른 클래스들이 표준화된 모양, 표준화된 property와 메소드를 갖도록 해주는 청사진을 만들기 위해 청사진을 사용함
// 단지 다른 클래스가 따라야 할 청사진(설계도)만을 제시했을 뿐 아무것도 구현하지 않음
// 만약 User 클래스를 상속한다면, 너는 sayHi랑 fullName을 구현해야 하고
// firstName 과 lastName 을 갖게 됨
// 그리고 추상 클래스는 인스턴스를 만드는 걸 허용하지 않음. new User() 작동 x
abstract class User {
constructor() {
// protedted는 추상 클래스로부터 상속받은 클래스들이
// property에 접근하도록 해줌
protedted firstName: string,
protedted lastName: string,
}
// 이 추상 클래스가 두 개의 메소드를 가지도록
abstract sayHi(name: string): string
abstract fullName(): string
}
class Player extends User {
sayHi(name: string) {
return `Hello ${name}. My name is ${this.fullName()}`
}
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
지금이 인터페이스를 써야 할 때! 인터페이스는 가벼움. 컴파일하면 JS로 바뀌지 않고 사라짐. 인터페이슬르 쓸 때 클래스가 특정 형태를 따르도록 어떻게 강제하냐가 문제!
인터페이스는 constructor나 abstract가 없음. 하지만 인터페이스는 오브젝트나 클래스의 모양을 묘사하도록 해줌
User 인터페이스를 추적할 수가 없음 왜냐면 인터페이스는 타입스크립트에서만 존재하기 때문
interface User {
firstName: string
lastName: string
sayHi(): string
fullName(): string
}
// implements라는 자바스크립트가 사용하지 않는 단어를 사용
class Player implements User {
// User 인터페이스를 상속하고 누락된 property들을 넣어서 타입스크립트를 만족시키기
constructor(
// 문제는 인터페이스를 상속할 때엔 property를 private로 만들지 못함
firstName: string,
lastName: string
) {}
sayHi(name: string) {
return `Hello ${name}. My name is ${this.fullName()}`
}
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
이게 인터페이스와 클래스의 가장 큰 차이로, 인터페이스는 고유한 사용처가 있음 그리고 얘네들은 클래스의 모양을 알려준다는 점에서 엄청 유용함. 그러면서도 추상 클래스를 사용할 때처럼 자바스크립트 코드로 컴파일되진 않음
문제는 인터페이스를 상속하는 것의 문제점 중 하나는 private과 protected property 들을 사용하지 못한다는 것과, firstName: string, lastName: string...
이 부분을 해 줄 construnctor가 없음.
당연하게도 만약 우리가 원한다면, 하나 이상의 인터페이스를 동시에 상속할 수도 있음
interface User {
firstName: string
lastName: string
sayHi(): string
fullName(): string
}
interface Human {
health: number
}
class Player implements User, Human {
constructor(
public firstName: string,
public lastName: string,
public health: number
) {}
sayHi(name: string) {
return `Hello ${name}. My name is ${this.fullName()}`
}
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
이건 굉장히 유용함. 예를 들어, 어댑터 패턴과 같은 디자인 패턴을 사용하여 팀과 함께 일할 때 인터페이스를 만들어두고 팀원이 원하는 각자의 방식으로 클래스를 상속하도록 하는 건 엄청나게 멋진 방법임! 만약 모두가 같은 인터페이스를 사용한다면 너는 같은 property 와 method 를 가지게 됨. 다시 한 번 말하지만, 이건 클래스가 아니지만 클래스의 모양을 특정할 수 있게 해주는 간단한 방법
오브젝트의 모양을 결정지을 수도 있지만 클래스의 모양을 특정짓기도 하고, 한 클래스에서 여러 개의 인터페이스를 상속할 수도 있음
그리고 인터페이스를 타입으로 지정할 수 있는 방법도 알아야 함. 클래스를 타입으로 쓸 수 있고 인터페이스도 타입으로 쓸 수 있음. argument에 설정할 수도 있고, 인터페이스를 리턴할 수도 있음
interface User {
firstName: string
lastName: string
sayHi(): string
fullName(): string
}
function makerUser(user: User): User {
// return new User() X
return ({
// 인터페이스의 내용물만 넣으면 됨
firstName: string
lastName: string
sayHi(): string
fullName(): string
})
}
// 인터페이스의 내용물만 넣으면 됨
makerUser({
firstName: 'nico',
lastName: 'las',
sayHi: () => 'string',
fullName: () => 'xx'
})
인터페이스는 네가 원하는 메소드와 property 를 클래스가 가지도록 강제할 수 있게 해줌. 그리고 인터페이스는 자바스크립트로 컴파일되지 않음. 추상 클래스와 비슷한 보호를 제공하지만, 자바스크립트 파일에서 보이지 않음. 추상 클래스르 쓰면 JS에선 일반적인 클래스로 바뀌는데 이건 파일 크기가 좀 더 커지고 추가 클래스가 만들어진다는 뜻임
만약 추상 클래스를 다른 클래스들이 특정 모양을 따르도록 하기 위한 용도로 쓴다면 같은 역할을 하는 인터페이스를 쓰는 게 더 좋음. class Player extends User
-> class Player implements User
첫 번째, 타입을 쓰고 싶다면 type 키워드를 사용
여기엔 다양한 옵션들이 있는데 한 가지 옵션은 오브젝트의 모양을 설명, 다른 옵션은 type alias를 만드는 것, 그리고 타입을 특정된 값으로 만드는 것
먼저 비교할 것은 TS에게 object shape를 알려줄 때, 둘 다 오브젝트의 모양과 타입을 알려주는 게 그 목표로 목적은 완전히 동일하고 둘 다 잘 수행함.
인터페이스 상속법은 객체지향 프로그래밍 컨셉과 매우 유사
나중에 property를 추가하고 싶다면, interface는 중복 선언이 가능
type PlayerA = { name: string }
type PlayerAA = PlayerA & { lastName: string }
const playerA: PlayerAA = { name: 'nico', lastName: 'las' }
// interface를 쓸 때
interface PlayerB { name: string }
interface PlayerB { lastName: string }
const playerB: PlayerB = { name: 'mico', lastName: 'las' }
원한다면 인터페이스와 타입 모두 추상 클래스를 대체해서 쓸 수 있음
type PlayerA = { firstName: string }
interface PlayerB { firstName: string }
// class User implements PlayerB {
class User implements PlayerA {
constructor(
public firstName: string
) {}
}
타입스크립트 커뮤니티에서는 클래스나 오브젝트의 모양을 정의하고 싶으면 인터페이스를 사용하고, 다른 모든 경우에는 타입을 쓰라고 하고 있음. 가장 눈에 띄는 차이점은 타입은 새 property 를 추가하기 위해 다시 선언될 수 없지만 인터페이스는 항상 상속이 가능하다는 것
Type Aliases과 인터페이스는 매우 유사하며 많은 경우 자유롭게 선택할 수 있습니다. 인터페이스의 거의 모든 기능은 type에서 사용할 수 있으며, 주요 차이점은 type을 다시 열어 새 속성을 추가할 수 없는 것입니다. 반면 인터페이스는 항상 확장 가능합니다.
결론: 대부분의 경우 개인 취향에 따라 선택 가능
(인터페이스 사용을 조금 더 추천)
https://nomadcoders.co/typescript-for-beginners/lectures/3683
다형성, 제네릭, 클래스 그리고 인터페이스를 모두 합쳐보자! polymorphism 다형성은 다른 모양의 코드를 가질 수 있게 해주는 것으로 다형성을 이룰 수 있는 방법은 제네릭을 사용하는 것으로 concrete 타입이 아니라 placeholder 타입임. 때가 되면 TS가 concrete 타입으로 바꿔 주므로 그냥 placeholder 타입을 선택
브라우저에서 쓰는 localStorage API와 비슷한 API를 가지는 만들어보기
// Storage는 TS에 의해 이미 선언된 JS 웹 스토리지 API를 위한 interface
// interface Storage {...} 이런 식으로 하면 Storage에 새 property를 추가하게 됨
interface SStorage<T> {
[key:string]: T
}
class LocalStorage<T> {
private storage: SStorage<T> = {}
// Create
set(key: string, value: T) {
if(this.storage[key] !== undefined){
return console.log(`${key}가 이미 존재합니다. update 호출 바랍니다.`)
}
this.storage[key] = value
}
// Read
get(key: string): T|void {
if(this.storage[key] === undefined){
return console.log(`${key}가 존재하지 않습니다.`)
}
return this.storage[key]
}
// Update
update(key: string, value: T) {
if(this.storage[key] !== undefined){
this.storage[key] = value
} else {
console.log(`${key}가 존재하지 않아 새로 만듭니다.`)
this.storage[key] = value
}
}
// Delete
remove(key: string) {
if(this.storage[key] === undefined){
return console.log(`${key}가 존재하지 않습니다.`)
}
delete this.storage[key]
}
clear() {
this.storage = {}
}
}
const stringsStorage = new LocalStorage<string>()
stringsStorage.get('cat') // : string
stringsStorage.set('hello', 'how are you')
const booleansStorage = new LocalStorage<boolean>()
stringsStorage.set('hello', true)
컴퓨터 시스템에 밑바닥부터 타입스크립트 프로젝트 설정하는 법!
NextJS, CRA 등을 사용하는 대부분의 사람들은, 수동으로 타입스크립트 프로젝트를 설정할 일이 거의 없음. 이런 프레임워크 라이브러리 패키지들은 우링 위해 만들어 줌. 그렇지만 우리가 설정 파일에 작성하는 것들을 쓰는 이유와 다룰 내용을 이해하는 것은 중요. 이걸 아주 가끔 스스로 수동으로 써야 할 일이 생길 수 있음. webpack을 쓰는 것과 비슷
블록체인의 PoC (개념증명)를 객체 지향 프로그래밍을 확용하는 타입스크립트로 만들어보면서 TS를 더 연습하고 더 많은 지식도 알아볼 것. NodeJS랑 VSCode가 설치되어 있어야 됨
# typechain이라는 폴더 생성
mkdir typechain
# vscode에서 열기
code typechain
# 새 nodejs 프로젝트 만들기
npm init -y
# typescript devDependencies에 설치
npm i -D typescript
src에 index.ts 파일 생성
// 바보같은 함수
const hello = () => "hi";
이 파일을 컴파일해서 자바스크립트 파일 받기. 이게 컴파일이 동작한다는 개념을 입증
이 파일의 이름은 tsconfig.json 이어야만 함. 이 파일이 있으면 vscode는 우리가 ts로 작업한다는 것을 즉시 알게 되고, 아주 훌륭한 자동완성 기능을 제공해 줄 것임.
# tsconfig.json 파일 생성.
touch tsconfig.json
여기엔 몇 가지 옵션이 있음
{
// 가장 먼저 타입스크립트에게 알려줘야 하는 것은, 어디에 타입스크립트 파일이 위치하는지
// include의 배열엔 우리가 JS로 컴파일하고 싶은 모든 디렉터리를 넣음
"include": [
"src", // TS가 src의 모든 파일을 확인한다는 것을 의미
],
"compilerOptions": {
// JS 파일이 생성될 디렉토리를 지정함. (TS는 컴파일러니까 .ts 파일들을 일반적인 JS로 컴파일 시켜줌)
"outDir": "build" // build라는 폴더에 만들어진 코드를 넣을 것이라 알려줌
}
}
package.json -> npm run build
{
...
"scripts": {
"build": "tsc"
}
...
}
그럼 build란 폴더가 생기고 이 안에는 index.js라는 파일이 있음
// build/index.js
// 일반적인 함수로 바뀜
// 타입스크립트가 이 코드를 컴파일해서 낮은 버전의 자바스크립트 코드로 바꿔줌
var hello = function () { return 'hi'; };
// src/index.ts
const hello = () => "hi";
이건 TS가 이 코드를 컴파일해서 낮은 버전의 JS 코드로 바꿔준 거임. 어디에서든 이해할 수 있는 더 호환성이 좋은 자바스크립트 코드로 바꾼 것.
원한다면 이 코드가 어떤 버전의 JS로 바뀔 지를 정할 수 있음
tsconfnig.json에서 compilerOptions 안에 target을 정의
{
"include": [
"src", // TS가 src의 모든 파일을 확인한다는 것을 의미
],
"compilerOptions": {
"outDir": "build",
"target": "es5" // 어떤 버전의 JS로 컴파일하고 싶은지
}
}
src/index.ts
class Block {
constructor(private data: string) {}
static hello() {
return 'hi';
}
}
npm run build
실행
build/index.js
TS가 클래스가 존재하지 않는 엄청 옛날 버전의 JS로 이걸 컴파일했음. 이건 정상 작동함
ES2022는 최소환의 호환성도 충족시키지 못할 수 있음. 이건 엄청 최신의 자바스크립트를 쓰겠다는 말
es5, es6가 적당
TSConfig Option: target - TypeScript
"최신 브라우저는 ES6의 모든 기능을 지원하므로, ES6를 사용하는 건 좋은 선택입니다."
공식 문서에서는, target 세팅은 어떤 자바스크립트의 기능이 다운레벨되는지를 바꾼다고 적혀있음.
고민할 일은 거의 없을 거지만 target이 무슨 역할을 하는지 알기
아무것도 적지 않으면 아마 es3 혹은 es5
tsconfig.json
{
"include": [
"src", // TS가 src의 모든 파일을 확인한다는 것을 의미
],
"compilerOptions": {
"outDir": "build",
"target": "es5",
"lib": [] // 합쳐진 라이브러리의 '정의 파일?'을 특정해 주는 역할을 한다고 적혀잇음
// 그러니까 그 정의 파일을 목표로 하는 런타임(실행) 환경을 알려주게 될 거임
}
}
library declaration
'목표로 하는 실행 환경을 나타낸다'고 적혀있음?
무슨 말이냐면, 이건 너의 자바스크립트 코드가 어디에서 동작할지를 알려준다는 것
어떤 환경인지를,
자바스크립트의 어떤 버전이 그 환경에서 사용되는지를 말이야.
테스트를 위해 우리의 코드가 es6를 지원하는 환경에서 실행될 거라고 지정하기
"DOM"은 이 코드를 브라우저 위에서 실행시킬거라고 써주는 것. 이건 브라우저를 위한 거임
{
"include": ["src"],
"compilerOptions": {
"outDir": "build",
"target": "es5",
"lib": ["ES6", "DOM"]
}
}
차이점은 만약 DOM을 lib에 포함시켜두고, TS 코드에서 document를 쓰면 타입스크립트는 document가 뭔지 알고있음
타스가 document가 가지고 있는 모든 이벤트와 메소드를 보여주고 있음.
우리가 TS에게 이 코드가 브라우저를 위해 작성되고 있다고 알려줘서, 이 모든 자동완성을 제공해 우리를 도와줌. 왜냐면 TS가 브라우저의 API와 타입들을 알고있기 때문
핵심은, lib에 작성된 이 줄을 기반으로 우린 타입스크립트에게 코드가 어디서 동작한 것인지를 알려줄 수 있음. 그러면 TS는 우리가 사용할 API가 뭔지 아니까 자동완성 기능을 제공해 줄 수 있음!
마음에서빠져나와삶속으로들어가라