나는 여태 js 를 이용했다. js 만 배웠기 때문이다. 주위에서 매우 많은 분들이 ts를 배우고 사용하라 하셨다.
코스도 끝났고, 이제 나만의 커리큘럼을 따라갈 수 있기 때문에 미뤄뒀던 TypeScript
를 배워보려합니다.
지난 프로젝트에서 js 를 사용했다. 다양한 객체 내에 다양한 함수들이 있었다. user.name
, user.nickname
, user.password
, attraction.name
, attraction.address
, stay.stayname
,post.userid
등등
왜인지 모르게 팀으로 작업하면서 의사소통의 문제인지 함수이름이 통일되지 않은 부분이 많았다. attraction
에서는 해당 객체의 이름이 name
인데 stay
에서는 stayname
이라고 한다던가. 비슷한 객체임에도 모두 달랐다. 심지어 같은 user
임에도 어떤 부분에서는 user.id
이지만 어떤 부분에서는 user.userId
라고 했다.
그럼 어떡해? 어떡하긴 그냥 받아보고 어디선가 문제가 생기면 일일이 console.log 를 찍어보고 어느 부분에서 undefined
가 나오는 걸 보고, 이건 왜 또 달라! 하면서 스트레스 받고 그랬던 거지.
TypeScript
는이런 문제들을 코드를 짤 때 미리 말해준다고 한다.
사실 내가 아직 ts 의 시작이라, 백엔드에서 만들어준 객체까지 체크를 해주는지는 모르겠다만... 그랬으면 좋겠지만.. 어쨌든
코드를 실행하기 전에 문제가 있다고 얘기를 해준다면 방금 마주했던 스트레스는 이제 다시 볼 일이 없을 거다.
그래서 가장 먼저 배워놓는다면 후에 어떤 프로젝트를 하던지 간에 다시 사용할 수 있을 것이다. 다시 일일이 log 찍으며 찾아 해매는 일은 없는 거지.
부푼 기대를 안고 배워봅시다.
새 프로젝트를 시작하기에 앞서 컴퓨터를 포맷했기 때문에
타입스크립트를 시작하면서 설치해줄 것들이 많다.
VSCODE 설치
NODE.JS 설치
NODEJS 들어가서 설치해준다. TYPESCRIPT 강의에서 사용하는 버전이 17.3 이라 17.3을 설치할까 했는데, 그냥 최신버전 설치했다.
선생님이 17.3 이상이면 된다고 했다. 뭐든 쌔거가 좋은 거 아니겠어요?
사실 안정버전이 17.3. 이상이었으면 그거 설치했을 텐데 홈페이지에 나와있는 안정버전이 16버전이라 18 버전 설치함.
본격적으로 강의를 들으며 시작해봅시다.
매우 중요한 특징 중 하나가 타입 지정이다.
이름조차 type
script 인 만큼 타입이 정해져있다.
// js
const name = "thovy"
이렇게 선언하면 되는 js 와 달리 ts
는
// ts
const name : string = "thovy"
타입을 함게 명시해줘야한다.
하지만 타입스크립트는 타입을 추론해주기 때문에, 첫 변수 선언에서 js 처럼 작성했다고 해서 걱정하지 않아도 된다. ts는 그 변수의 타입을 추론해 뒤에 그 변수의 타입이 변하지 않는다면, 오류를 발생시키지 않는다. 하지만 뒤에 그 변수의 타입을 잘못 적는다면, 문제가 발생한다.
// ts
let name = "thovy"
// 이렇게 적어도 name 이 string 이라고 추론하고
let name = "harry"
// 라고 하면 에러가 나지 않는다.
//하지만
let name = true // boolean 타입
// 라고 변수의 타입이 잘못되면 에러가 난다. 타입설정을 해주지 않았지만 타입을 추론해놓은 거다.
타입스크립트가 타입을 추론해기 때문에 타입을 명시해주지 않는 것을 기본으로 하고 최소한으로 필요할 때만 명시해주는 것을 권장한다는데,
내 생각엔 단순한 변수가 아니면 타입을 명시해주는 게 낫다고 생각한다. 코드가 조금 길어지고 가독성은 조금 떨어지지만...일주일 전에 작성한 코드를 까먹어버리는 나의 댕청한 머리로 여러 사람과 함께 코드를 짜야하는 상황에서는 변수의 타입을 명시해주는 것이 깔끔하고 빠른 코드 작성과 이해에 큰 도움이 될 거라 생각한다.
let array = []
// 라고 하면 array의 타입은 any[] 가 되어 무엇이든 들어갈 수 있게 되고,
array.push('1')
// 이라는 string 을 넣어도 된다.
let array = [1,2,3]
// 이라고 하면 array 의 타입이 number[] 로 추론되어 숫자만 들어갈 수 있게되고,
array.push('1')
// 이라는 string 은 넣을 수 없게 된다.
let array : number[] = []
// 이라고 하면 array 의 타입이 명시적으로 number[] 가 되므로,
// array.push('1') 같은 바보같은 명령어는 쓰지 않겠지
const name: readonly string = 'thovy'
// 라고 readonly 선언하면
const name = 'harray'
// 라고 변경할 수가 없다. 저렇게 변경하려하면 빨간 밑줄이 생길 거다.
변경하면 안되는 값들을 선언할 때 readonly
를 사용하면 실수로라도 변경되는 것을 방지 할 수 있을 거다.
파이썬에서 들어본 것 같은 tuple 이라는 것도 있단다.
평범한 array 같은데 각 위치별로 타입을 지정해놓을 수 있는 거다.
배열에 프로그래머의 [이름, 생년월일, 활동여부] 를 넣어보자.
const programmer: [string, number, boolean] = ['thovy', 590703, true]
이렇게 짜여진 배열을 변경하려고 하면
programmer[0] = 950703 // error
잘못 된 위치에 잘못된 값을 넣으려 하면 에러가 발생된다. 왜냐면 첫번째[0] 자리는 string 이니까.
여기에 readonly 를 사용하면 알맞은 타입을 넣더라도 수정되는 것을 막을 수 있겠지?
const programmer: readonly [string, number, boolean] = ['thovy', 590703, true]
programmer[0] = 'harray' // error
하지만 타입을 정해주기 애매할 때는?
undefined 나 null 이 들어갈 수도 있다면?
type Programmer = {
name:string
age?:number
}
이렇게 ?
물음표를 쓰면 age
의 타입은 number
혹은 undefined
가 된다.
age 를 넣지 않는다면 undefined 가 되겠지? 그러니가 없어도 된다는 거지.
API 에서 응답을 받는데 그 변수의 타입을 모른다면?
UNKNOWN
let a : unknown;
// a 의 타입을 모를 때
if (typeof a === 'number') {
let b = a + 1
}
if (typeof a === 'string') {
let b = a.toUpperCase();
}
type 을 알 수 없으니 저렇게 적어놓을 수 있는 것.
만약에 a 가 어떤 타입으로든 한 번 사용되면, 그 뒤 부터는 앞서 사용된 타입으로 사용해야한다.
function add (a, b) {
return a + b;
}
라고 했을 때 이상한 건 없지만 ts 는 빨간 밑줄을 표시할 거다.
왜냐면 a b 가 뭔지 모르기 때문.
function add (a:number, b:number) {
return a + b;
}
이렇게 명시해줘야한다. 어떤 인자들을 받는지.
add(a:number,b:number):number{ ...
라고 또 명시할 수도 있지만 당연한 건 알아서 추론한다.
화살표 함수도 같다. 명시해줘야한다.
const addd = (a:number, b:number) => a+b
함수마다 명시를 해야하는게 귀찮다? 그럼 타입을 만들어보자!
type Add = (a:number, b:number) => number;
// Add 라는 type 을 만들어 준 것.
const add: Add = (a,b) => a+b
// add 라는 게 Add 타입이라고 선언해주고 나머지 인자들은 선언해주지 않았음.
// 그래도 Add 타입을 미리 선언해놓았기 때문에 알아서 number 로 알아들음.
type Logging = {
arr: number[] :void
}
const log: Logging = (arr) => {
arr.forEach(i => console.log(i))
}
이런 식으로 Logging
이라는 타입을 만들어 log
라는 함수를 사용하려고 할 때,
지금 log 라는 함수의 인자로는 number[]
만 들어갈 수 있다.
그래서
log([1, 2, 3]) // 1 2 3
log(['떼잉','고양이','치즈']) // error
string[]
을 넣으면 에러가 나올 거다.
그런데 우리는 다양한 값을 넣고 싶어
그럼 overloading
을 해보자
type Logging = {
arr: number[] :void
arr: string[] :void
}
이렇게 되면 number[]
, string[]
모두 잘 출력 될 거다.
하지만
log([1, 2, '고양이', '4마리'])
는 또다시 에러가 날거다.
그럼
arr:(number[] | string[]) :void
를 추가해주면 되지만, 이렇게 하나하나 추가해주는 것보다 좋은 방법이 바로
generic
polymorphism
을 이용하는 것(다형성)
type Logging = {
<T>(arr: T[]):void
}
라고 한다면 어떤 것을 넣던 간에, 그 때에 맞춰서 타입스크립트가 인자로 받을 타입을 알아서 바꿔?준다.
number[]
타입만 왔으면 그렇게 설정되어있다고 알려주고,
같이 들어오면 |
표시를 사용해 같이 사용할 수 있다고 알려주고,
때에 따라 다양하게 변형된다. 우리는 타입에 신경쓰지 않고 사용하면 되는 거지.
이런식으로.
<T>
는 뭐라고 적던 상관 없음.T
나V
를 많이 사용한단다.TypePlaceholder
any
를 사용하면
const log: Logging = (arr) => {arr[0]}
const a = log([1,2,'cat'])
a.toUpperCase()
라고 했을 때 에러가 표시 되지 않은채 실행했을 때 에러가 남.
1
는 uppercase 가 없잖아.
그런데 any
는 뭐든 가능하다고 하니, 에러표시를 하지 않을 거고.
근데 generic
은 이런 경우에 빨간 밑줄로 에러를 표시 해줄 거임.
class User {
constructor(
private firstName:string,
private lastName:string,
private id:string,
public nickname:string
)
}
const thovy = new User('King', 'Thovy','thovy123','thovy');
이렇게 private 으로 해놓으면 함부로 가져다 쓸 수 없다.
객체지향 언어에서 볼 수 있는 특징. private
마치 자바에서 사용하는 거랑 비슷한 거 같다.
그래서
thovy.nickname // 'thovy'
thovy.name // error
private
인 name
은 빨간 밑줄이 그이며 사용할 수 없을 거다.
js 로는 할 수 없는 부분.
abstract class User {
constructor(
private firstName:string,
private lastName:string,
private id:string,
public nickname:string
)
}
class Customer extends User{
}
이렇게 되면 User 를 바로 사용할 수 없다. User
는 오로지 상속만 할 수 있는 클래스가 된다.
const thovy = new Customer('King','Thovy','thovy123','thovy');
라고 해야함. 직접적으로 인스턴스를 만들 수 없다.
abstract class User {
constructor(
private firstName:string,
private lastName:string,
private id:string,
public nickname:string
){}
getFullName()
return `${this.firstname} ${lastname}`
}
class Customer extends User{
}
추상 클래스에 getFullName
이라는 걸 추가해주고, 그걸 상속받았다면,
thovy.getFullname()
도 가능하다.
하지만
private getFullName(){...}
이 된다면( private
이 된다면) 사용할 수 없다.
private
은 프로퍼티 뿐만 아니라 메서드에도 적을 수 있다.
자바같다.
private
으로 하면 상속받은 클래스에서도 private
객체를 사용할 수 없다.
하지만 protected
라면 클래스 밖에서는 사용할 수 없지만, 상속받은 클래스 안에서는 객체를 사용할 수 있다.
abstract class User {
constructor(
protected firstName:string,
private lastName:string,
private id:string,
public nickname:string
){}
getFullName()
return `${this.firstname} ${lastname}`
}
class Customer extends User{
getFullname(){
console.log(this.firstName) // firstName 이 출력된다
console.log(this.lastName) // error
}
}
readonly
"type Words = {
[key:string]:string
}
class Dict {
private words:Words;
}
이렇게 하면 private words 부분에서 빨간 밑줄이 생기는데,
이니셜라이즈가 없으니,
선언한 뒤에 컨스트럭터를 만들어 초기화 해주자.
type Words = {
[key:string]:string
}
class Dict {
private words:Words;
constructor(){
this.words ={}
}
}
class Word {
constructor(
public term:string,
public def :string,
){}
}
const kimchi = new Word("kimchi","한국의 음식");
이렇게 되면 kimchi
라는 변수는 kimchi
라는 Dict
의 term이 kimchi
고, def 가 한국의 음식
인 Word
를 담은 변수가 되는 거다. 그래서 kimchi
변수를 호출하면 자연스럽게 Dict 오브젝트에 {kimchi,한국의음식}
이 담겨 응답되겠지?
type Words = {
[key:string]:string
}
class Dict {
private words:Words;
constructor(){
this.words ={}
}
add(word:Word){ // dict 에 add라는 객체 추가 기능 추가
if(this.words[word.term] === undifined){
this.words[word.term] = word.def
}
}
def(term:string){ // dict 에 def 라는 검색?기능 추가
return this.words[term]
}
}
class Word {
constructor(
public term:string,
public def :string,
){}
}
const kimchi = new Word("kimchi","한국의 음식");
const dict = new Dict()
dict.add(kimchi); // dict 이라는 Dict 타입의 새 변수를 만들고 거기에 kimchi 변수를 넣음(add)
dict.def("kimchi"); // 거기에서 def 라는 메서드를 이용함
그러면 이렇게 나올 거다.
그럼 앞으로 단어를 삭제하고, 수정하는 메서드도 추가하기 쉬울 거다.
extends
와는 다르게 implements
는 js 에는 없다. 그래서 코드가 더욱! 가벼워진다.
그래서 interface
와 implements
를 사용하면 js 에서는 어떤 것도 나오지 않고 상속받는 클래스만 표시된다.
그럼 코드가 더욱 가벼워진다.
type PlayerA = {
name:string
}
const playerA: PlayerA ={
name:'thovy'
}
////////
interface PlayerB = {
name:string
}
const playerB: PlayerB ={
name:'thovy'
}
이렇게 보면 똑같아 보이지만, 타입을 상속할 때는 달라진다.
lastName
이라는 속성을 추가하고 싶다면
type PlayerA = {
name:string
}
type PlayerAA = PlayerA & {
lastName:string
}
const playerA: PlayerA ={
name:'thovy'
lastname:'King'
}
////////
interface PlayerB = {
name:string
}
interface PlayerB {
lastName:string
}
const playerB: PlayerB ={
name:'thovy'
lastName:'King'
}
interface SStorage { // Storage는 이미 ts 기본 interface 가 있으므로 SStorage로
[key:string] : T
}
class LocalStorage<T> {
private storage: SStorage<T> ={}
set(key:string, value:T){
this.storage[key] =value;
}
remove(key:string){
delete this.storage[key]
}
get(key:string):T{
return this.storage[key]
}
clear(){
this.storage = {}
}
}
LocalStorage 에서 T 를 반환하도록 하므로, T 에 어떤 걸 넣어주느냐에 따라
새로운 변수를 만들 수 있음.
const stringSStorage = new LocalStorage<string>()
const booleanSStorage = new LocalStorage<boolean>()
vscode 를 열고 터미널에 npm init -y
를 입력해 node.js 프로젝트 생성.
package.json
에서 main
지우기
test
지우기
터미널에 npm i -D typescript
를 입력해 typescript
설치
설치 확인
src
폴더 생성 - 그 안에 index.ts
파일 생성
7.tsconfig.json
파일 생성(touch tsconfig.json 은 왜 안 되지)
7-1. 코드 입력
{
"include": ["src"],
"compilerOptions": {
"outDir": "build"
}
}
include
: ts 가 include 안에 있는 폴더를 모두 본다는 것. 여기선 src 를 지정해줬으니 src 에 있는 모든 파일을 확인해보고 compile 할 거다.
compilerOptions
outDir
: complie 된 파일을 어디에 저장할 건지. 여기선 build 라고 했으니 build 폴더안에 저장한다.
저장하고 npm run build
를 실행.
build
의 index.js
파일이 생성되는 지 확인
package.json
에서 target
을 입력하면
이렇게 버전을 지정할 수 있다.
npm run build
ES3 에서는 const 도 없고 화살표 함수도 없다. ES6 로 버전을 지정해주니 const 와 화살표 함수가 표현된다.
lib 는 프로젝트가 어떤 환경에서 실행될지 정해주는 건데,
이렇게 DOM
을 적으면 웹브라우저 환경에서 실행된다는 것을 애기해준다.
그래서 ts 파일에 코드를 적을 때
document 라고 치면
이렇게 document 내부의 메서드들이 나오지만.
lib 에서 DOM
을 지우면
브라우저 환경이 아니라고 생각해 document 를 입력해도 알아듣지 못한다.
마치 백엔드 서버라고 생각하는 거지.
npm i -D ts-node
실제 배포에서는 사용하지 않고 개발환경에서 사용. 계속 js 로 변환하면서 빌드하고 그러면 오래걸리니까.
ts-node 를 추가하면 컴파일 없이 ts 코드를 실행해준다.
script 에 'ts-node src/index'
(확장자는 없어도 되고 ts 라고 해도된다. ts를 실행하는 script 니까 js 는 안되겠지?)
npm i nodemon
커멘드를 재실행해줘서 서버를 재시작할 필요가 없다.
script 에 nodemon --exec
dependencies 에 우리가 설치한 친구들이 잘 설치됐나 확인하고
npm run dev
이렇게 되면, 코드를 바꿔서 저장하면 곧바로 서버가 재실행되며 변경된 사항이 적용된다. react 는 이걸 모두 포함하고 있던 거지.
정말 react 는 최고야. 사랑해요 저커버그
...
강의를 전혀 이해하지 못함.
this.blockchain 이라고 한 거를
[...this.blockchain] 이라고 하니까 보호가 되던데?!
알아봐야겠다. 그거는 이번 react 프로젝트에서도 사용했었던 거니까.
어쨋든 ts 를 배운 것 같다..?
시간이 없으니 바로 시작해보자.