이 글은 NomadCoders 님의 강의를 수강하고 정리한 글입니다.

강의 하나의 분량을 5분 정도로 끊으시는데 아주 손에 땀을 쥐게 하는 5분이었다. 내 기준 굉장히 타이트했기 때문에 잠시도 한 눈을 팔 수 없었다. 중간 중간 찰진 욕을 들으면 넘 웃겨서 잠시 긴장이 풀리지만 금방 정신을 차려야 한다… 암튼 결론은 니꼴라스님 강의 최고시다 🥰

니꼬님 말투를 살려서 정리해보았다


내 정보를 파라미터로 보내 호출하면 그 내용을 한 문장으로 반환해주는 함수임

먼저 그냥 js 문법대로 작성해보자

const name = "Yun Jeong",
    age = 25,
    gender = "female";

const sayHi = (name, age, gender) => {
    console.log(`Hello ${name}, you are ${age}, you are a ${gender}`);
}

sayHi(name, age, gender);

export {};

js에서는 함수를 호출할 때 파라미터를 선언한 대로 넣어주든 안 넣든 오류가 나지 않는다.

근데 Typescript(이하 ts)에서는…

const name = "Yun Jeong",
    age = 25,
    gender = "female";

const sayHi = (name, age, gender) => {
    console.log(`Hello ${name}, you are ${age}, you are a ${gender}`);
}

sayHi(name, age);

export {};

똑같이 이렇게 코드를 짰을 때 오류를 빵!! 하고 보여줘. 멍청한 개발자의 실수를 미연에 방지해주는거;; js에서는 이런거 본 적 없을껄? 그냥 Hello Yun Jeong, you are a 25, you are a undefined 가 출력되겠지

만약 sayHi 함수의 gender를 optional하게 받고싶다면?

const name = "Yun Jeong",
    age = 25,
    gender = "female";

const sayHi = (name, age, gender?) => {
    console.log(`Hello ${name}, you are ${age}, you are a ${gender}`);
}

sayHi(name, age);

export {};

이렇게 function을 정의할 때 ?(Optional)를 사용하면 됨. Cool~


이제 function을 더 Typed 하게 만들어볼까

const sayHi = (name:string, age:number, gender:string) => {
    console.log(`Hello ${name}, you are ${age}, you are a ${gender}`);
}

sayHi("YUMYUMYUM", 5000, "female");

export {};

개발자가 실수할 수 있는 부분을 미리 방지해주는군, 더 예측 가능한 코드가 되겠어

이제는 함수의 리턴 값을 Typed 하게 바꿔보자

const sayHi = (name: string, age: number, gender: string): void => {
    console.log(`Hello ${name}, you are ${age}, you are a ${gender}`);
}

sayHi("YUMYUMYUM", 5000, "female");

export {};

정확하게 어떤 값이 리턴되는지 알 수 있겠군!!


아니 근데 yarn start 매번 입력해야 하는지..? No!

tsc-watch라는 라이브러리를 사용하면 더욱 쉽게 개발할 수 있다. tsh-watch는 ts에 변경 사항이 발생하는 것을 보고있다가 재빠르게 업데이트 해준다

yarn add tsc-watch --dev

로 패키지를 설치해주고 package.json의 start script를 아래처럼 변경해주자

"scripts": {
    "start": "tsc-watch --onSuccess \" node dist/index.js \" "
},

위에 –onSuccess는 ts compile(tsc)이 성공했을 시에만 동작하라는 뜻

그리고 root에 몽땅 때려넣어진 파일들을 정리해주자

  • compile후에 생긴 js파일은 dist/ 밑으로
  • ts파일은 src/ 밑으로

파일 위치를 변경하고 난 후에는 config 옵션도 변경해주어야 한다

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "ES2015",
        "sourceMap": true,
        "outDir": "dist"
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules"
    ]
}

요로케


만약 파라미터로 object를 넘기고싶다면 어떻게 해야할까?

const person = {
    name: "Yun Jeong",
    age: 25,
    gender: 25
}

const sayHi = (name: string, age: number, gender: string): string => {
    return`Hello ${name}, you are ${age}, you are a ${gender}`;
}

console.log(sayHi(person));

export {};

이렇게 하면 될까? No. 인터페이스를 사용해보삼

interface Human {
    name: string;
    age: number;
    gender: string;
}

const person = {
    name: "Yun Jeong",
    age: 25,
    gender: "female"
}

const sayHi = (person: Human): string => {
    return`Hello ${person.name}, you are ${person.age}, you are a ${person.gender}`;
}

console.log(sayHi(person));

export {};

짠, 이제 내 Object는 더 예측가능해졌음

그런데 TS는 실제로 사용해 봤을 때 그 장점이 더욱 잘 드러남

그러니 얼른 무언가를 만들어 보자!


interface는 js로 컴파일되지 않아. 근데 가끔 인터페이스를 js에 넣고싶을 때가 있을텐데, 이럴 때는 ^class^를 사용하면 됨

class!!!! js에서는 class를 사용할 때 class의 속성들에 대해 묘사하지 않는다. 하지만 ts에서는 class의 속성들이 어떤 타입이어야 하는지 하는지, 어떤 권한을 가져야 하는지 상세하게 알려줘야 한다

interface를 class로 바꿔보자

class Human {
    public name: string;
    public age: number;
    public gender: string;
    constructor(name: string, age: number, gender: string) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
}

const me = new Human("Veronica", 25, "female");

const sayHi = (person: Human): string => {
    return`Hello ${person.name}, you are ${person.age}, you are a ${person.gender}`;
}

console.log(sayHi(me));

export {};

인터페이스를 쓰거나 클래스를 쓰는 것은 상황에 따라 선택해서 쓰면 된다. 사실 interface는 ts에 적용하기에 더 적합한 것이고 react, express 등등을 사용한다면 class를 사용해야 할 것이다

ts의 private/public을 사용하여 보호해야 할 데이터를 지정 해줌으로써 더 secure한 코딩이 가능해진다. 외부에 알리고 싶지 않은 속성들을 정확히 구분하는 것!


You know what, F**K THEORY. 이론은 그만하고 바로 블록 구조를 만들어보자

(노마드 코인 강의 들어야겠다…. 그래야 ts의 장점을 피부로 느낄 수 있다고 한다)

class Block {
    public index: number;
    public hash: string;
    public previousHash: string;
    public data: string;
    public timestamp: number;
    constructor(index: number, hash: string, previousHash: string, data: string, timestamp: number) {
        this.index = index;
        this.hash = hash;
        this.previousHash = previousHash;
        this.data = data;
        this.timestamp = timestamp;
    }
}

const genesisBlock: Block = new Block(0, "2020202020", "", "hello", 123456);

let blockchain: Block[] = [genesisBlock];

console.log(blockchain);

export {};

위의 코드를 실행하면..

[
    Block {
    index: 0,
    hash: '2020202020',
    previousHash: '',
    data: 'hello',
    timestamp: 123456
    }
]

멋찌지?

게다가 ts가 blockchain에 진짜 Block만 담기는 지 검사 해 주거든

바보같이 blockchain.push(“trash”) 같은 게 작동할 수가 없다는 것이야!!


이제 진짜 hash 함수를 사용 해서 블록을 생성해보자. crypto-js라는 라이브러리를 사용 할 것임

나는 calcualteBlockHash라는 함수를 만들어서 해시값을 구하게 하고 싶은데, class 내부에서 일반적인 method 선언 문법을 사용하게 된다면 해당 method는 반드시 블록을 생성했을 때에만 사용할 수 있게 된다. 으악!!!

이럴 때는 함수를 static으로 선언하면 해결할 수 있다

static으로 선언하지 않고 함수를 호출하려고 하면 vsc에서 아래와 같은 에러를 방출한다

Property ‘calculateBlockHash’ does not exist on type ‘typeof Block’.ts(2339)

추가적으로 util처럼 사용할 다른 함수들도 만들어 보자.

import * as CryptoJS from 'crypto-js';

class Block {
    public index: number;
    public hash: string;
    public previousHash: string;
    public data: string;
    public timestamp: number;

    static calculateBlockHash = (
        index: number,
        previousHash: string,
        data: string,
        timestamp: number
    ): string => CryptoJS.SHA256(index + previousHash + timestamp + data).toString();

    constructor(index: number, hash: string, previousHash: string, data: string, timestamp: number) {
        this.index = index;
        this.hash = hash;
        this.previousHash = previousHash;
        this.data = data;
        this.timestamp = timestamp;
    }
}

const genesisBlock: Block = new Block(0, "2020202020", "", "hello", 123456);

let blockchain: Block[] = [genesisBlock];

const getBlockchain = () : Block[] => blockchain;

const getLatestBlock = () : Block => blockchain[blockchain.length - 1];

const getNewTimesStamp = () : number => Math.round(new Date().getTime() / 1000);

console.log(blockchain);

export {};

완성~!~! 아주 beautiful하고 predictable 하다. (predictable이 아주 중요한 ts 속성인 듯)


이제 새로운 블록을 만들어보자

import * as CryptoJS from 'crypto-js'; 

class Block {
    public index: number;
    public hash: string;
    public previousHash: string;
    public data: string;
    public timestamp: number;

    static calculateBlockHash = (
        index: number, 
        previousHash: string, 
        data: string, 
        timestamp: number
    ): string => CryptoJS.SHA256(index + previousHash + timestamp + data).toString();

    constructor(index: number, hash: string, previousHash: string, data: string, timestamp: number) {
        this.index = index;
        this.hash = hash;
        this.previousHash = previousHash;
        this.data = data;
        this.timestamp = timestamp;
    }
}

const genesisBlock: Block = new Block(0, "2020202020", "", "hello", 123456);

let blockchain: Block[] = [genesisBlock];

const getBlockchain = () : Block[] => blockchain;

const getLatestBlock = () : Block => blockchain[blockchain.length - 1];

const getNewTimesStamp = () : number => Math.round(new Date().getTime() / 1000);

const createNewBlock = (data: string) : Block => {
    const previousBlock: Block = getLatestBlock();
    const newIndex: number = previousBlock.index + 1;
    const nextTimestamp: number = getNewTimesStamp();
    const nextHash: string = Block.calculateBlockHash(newIndex, previousBlock.hash, data, nextTimestamp);
    const newBlock: Block = new Block(newIndex, nextHash, previousBlock.hash, data, nextTimestamp);
    return newBlock;
}

console.log(createNewBlock("hello"));
console.log(createNewBlock("byebye"));
console.log(blockchain);

export {};

여기서 콘솔을 찍고 나면 새로 생성한 블록의 index가 전부 1로 나오는데, 버그가 아니다. 아직 blockchain에 블록을 push(add)하지 않았기 때문이다


블록을 push하기 전에 validation을 진행해보자

비트코인 클론 강좌에서는 다양한 validation을 다루지만 이 강좌에서는 Block의 구조가 유효한지에 대해서만 검사한다

import * as CryptoJS from 'crypto-js'; 

class Block {
    static calculateBlockHash = (
        index: number, 
        previousHash: string, 
        data: string, 
        timestamp: number
    ): string => CryptoJS.SHA256(index + previousHash + timestamp + data).toString();

    static validateStructure = (aBlock: Block): boolean =>
        typeof aBlock.index === "number" &&
        typeof aBlock.hash === "string" &&
        typeof aBlock.previousHash === "string" &&
        typeof aBlock.data ==="string" &&
        typeof aBlock.timestamp === "number";

    public index: number;
    public hash: string;
    public previousHash: string;
    public data: string;
    public timestamp: number;

    constructor(index: number, hash: string, previousHash: string, data: string, timestamp: number) {
        this.index = index;
        this.hash = hash;
        this.previousHash = previousHash;
        this.data = data;
        this.timestamp = timestamp;
    }
}

const genesisBlock: Block = new Block(0, "2020202020", "", "hello", 123456);

let blockchain: Block[] = [genesisBlock];

const getBlockchain = () : Block[] => blockchain;

const getLatestBlock = () : Block => blockchain[blockchain.length - 1];

const getNewTimesStamp = () : number => Math.round(new Date().getTime() / 1000);

const createNewBlock = (data: string) : Block => {
    const previousBlock: Block = getLatestBlock();
    const newIndex: number = previousBlock.index + 1;
    const nextTimestamp: number = getNewTimesStamp();
    const nextHash: string = Block.calculateBlockHash(newIndex, previousBlock.hash, data, nextTimestamp);
    const newBlock: Block = new Block(newIndex, nextHash, previousBlock.hash, data, nextTimestamp);
    return newBlock;
}

const isBlockValid = (candidateBlock: Block, previousBlock: Block): boolean => {
    if (!Block.validateStructure(candidateBlock)) {
        return false;
    } else if (previousBlock.index + 1 !== candidateBlock.index) {
        return false;
    } else if (previousBlock.hash !== candidateBlock.previousHash) {
        return false;
    }
}

export {};

근데 아직 완성된 것이 아니다! validation할 때에 실제 해시값을 계산해서 검사해야 한다.


import * as CryptoJS from 'crypto-js';

class Block {
    static calculateBlockHash = (
        index: number,
        previousHash: string, 
        data: string, 
        timestamp: number
    ): string => CryptoJS.SHA256(index + previousHash + timestamp + data).toString();

    static validateStructure = (aBlock: Block): boolean => 
        typeof aBlock.index === "number" && 
        typeof aBlock.hash === "string" && 
        typeof aBlock.previousHash === "string" && 
        typeof aBlock.data ==="string" && 
        typeof aBlock.timestamp === "number";

    public index: number;
    public hash: string;
    public previousHash: string;
    public data: string;
    public timestamp: number;
    
    constructor(index: number, hash: string, previousHash: string, data: string, timestamp: number) {
        this.index = index;
        this.hash = hash;
        this.previousHash = previousHash;
        this.data = data;
        this.timestamp = timestamp;
    }
}

const genesisBlock: Block = new Block(0, "2020202020", "", "hello", 123456);

let blockchain: Block[] = [genesisBlock];

const getBlockchain = () : Block[] => blockchain;

const getLatestBlock = () : Block => blockchain[blockchain.length - 1];

const getNewTimesStamp = () : number => Math.round(new Date().getTime() / 1000);

const createNewBlock = (data: string) : Block => {
    const previousBlock: Block = getLatestBlock();
    const newIndex: number = previousBlock.index + 1;
    const nextTimestamp: number = getNewTimesStamp();
    const nextHash: string = Block.calculateBlockHash(newIndex, previousBlock.hash, data, nextTimestamp);
    const newBlock: Block = new Block(newIndex, nextHash, previousBlock.hash, data, nextTimestamp);
    return newBlock;
}

const getHashforBlock = (aBlock: Block) : string => Block.calculateBlockHash(aBlock.index, aBlock.previousHash, aBlock.data, aBlock.timestamp);

const isBlockValid = (candidateBlock: Block, previousBlock: Block): boolean => {
    if (!Block.validateStructure(candidateBlock)) {
        return false;
    } else if (previousBlock.index + 1 !== candidateBlock.index) {
        return false;
    } else if (previousBlock.hash !== candidateBlock.previousHash) {
        return false;
    } else if (getHashforBlock(candidateBlock) !== candidateBlock.hash) {
        return false;
    } else {
        return true;
    }
}

const addBlock = (candidateBlock: Block) : void => {
    if(isBlockValid(candidateBlock, getLatestBlock())) {
        blockchain.push(candidateBlock);
    }
}

export {};

완성! 하지만 아직 새로 생성된 블록을 blockchain에 추가하는 부분이 빠져있다

블록을 새로 생성한 이후에 addBlock을 사용하여서 blockchain에 등록해주자. createNewBlock 함수를 아래처럼 수정하면 된다

const createNewBlock = (data: string) : Block => {
    const previousBlock: Block = getLatestBlock();
    const newIndex: number = previousBlock.index + 1;
    const nextTimestamp: number = getNewTimeStamp();
    const nextHash: string = Block.calculateBlockHash(newIndex, previousBlock.hash, data, nextTimestamp);
    const newBlock: Block = new Block(newIndex, nextHash, previousBlock.hash, data, nextTimestamp);
    addBlock(newBlock);
    return newBlock;
}

이제 실제로 createNewBlock을 호출해보고 blockchain을 출력해보자.

createNewBlock X 3

createNewBlock("second block");
createNewBlock("thrid block");
createNewBlock("fourth block");
console.log(blockchain);

콘솔 창에 출력된 것을 보면 블록 3개가 추가된 것을 확인할 수 있다. hash 값을 비교해보면 정상적으로 hash 계산이 수행되었다는 것도 알 수 있다.

[
    {
        index: 0,
        hash: '2020202020',
        previousHash: '',
        data: 'hello',
        timestamp: 123456
    },
    {
        index: 1,
        hash:
            '9a2d7fbb2f3e016198489be442d1f9da5b09918d2c90ee58279ea2848ae759bf',
        previousHash: '2020202020',
        data: 'second block',
        timestamp: 1569300581
    },
    {
        index: 2,
        hash:
            'f90706bf59d0ae76c31314ef79653ef6502634966a93144a7dd0e1901c53153b',
        previousHash:
            '9a2d7fbb2f3e016198489be442d1f9da5b09918d2c90ee58279ea2848ae759bf',
        data: 'thrid block',
        timestamp: 1569300581
    },
    {
        index: 3,
        hash:
            '82c3343686313fb5ed1bba893329201b11d0fd94acddb7ef9071582deeb65e7a',
        previousHash:
            'f90706bf59d0ae76c31314ef79653ef6502634966a93144a7dd0e1901c53153b',
        data: 'fourth block',
        timestamp: 1569300581
    }
]

끄읏 -

crtf

쟈란 -