9시 24분
Typescript로 블록체인 만들기 본문
노마드코더 강의를 보면서 정리한 글입니다.
Typescript란?
타입스크립트는 자바스크립트에 타입을 부여한, 자바스크립트의 슈퍼셋 언어로, 자바스크립트의 확장된 언어라고 볼 수 있다.
확장자로는 .ts를 사용하며, 컴파일의 결과물로 자바스크립트 코드를 출력한다.
타입스크립트는 왜 배워야 하는걸까? 타입스크립트는 아래 2가지 관점에서 자바스크립트 코드의 품질과 개발 생산성을 높일 수 있다.
- 에러의 사전 방지
- 코드 가이드 및 자동 완성 (개발 생산성 확장)
Setting
우선 npm init 명령어를 통해 package.json 파일을 만들어주자.
처음에는 패키지 이름을 typescript로 했다가 npm install 과정에서 이름이 겹쳐 npm install이 되지 않았다. 그러므로 패키지 이름을 다르게 하자 ~
그 다음에 npm으로 typescript를 설치해주었다.
npm install typescript --save-dev
그리고 tsconfig.json 파일을 만들어 타입스크립트에게 어떻게 자바스크립트로 변환하는지 알려주면서 몇몇 옵션을 주자.
{
"compilerOptions": {
"module": "commonjs",
"target": "ESnext",
"sourceMap": true
},
"include": ["index.ts"],
"exclude": ["node_modules"]
}
- module: 빌드와 모듈 방식을 commonjs 방식으로 한다.
- target: 빌드 결과물을 esnext 방식으로 한다.
- sourceMap: true로 할 시, map 파일도 함께 생성한다.
- include: 컴파일할 파일의 경로
- exclude: 컴파일에서 제외할 파일
그리고 index.ts 파일을 생성해보자.
alert("hello");
타입스크립트를 컴파일할 때는 npx tsc로 컴파일을 한다. (npm으로 설치한 경우)
그러면 위와 같이 index.js와 index.map 파일이 생성된 것을 볼 수 있다.
하지만, 컴파일 후 js를 일일이 실행하는 것은 번거로운 일이다. 그러므로 명령어 하나로 컴파일 및 실행을 하도록 해보자.
아래와 같이 package.json의 scripts 부분에 start와 prestart를 추가해준다.
npm start 명령어를 치면 prestart가 실행된 뒤 start가 실행되어 컴파일 > js 실행 순으로 동작하게 된다.
index.js의 내용을
console.log("hello");
로 바꾼 뒤, npm start를 해보면 ...
잘 동작하는 것을 확인할 수 있다 !!
Typescript 첫 시작
타입스크립트는 'Type'이 있는 언어로, 변수와 데이터가 어떤 종류인지 설정을 해주어야 한다는 뜻이다.
컴파일러가 내가 맞게 하고 있는지 아닌지를 알려주기 때문에, 코드가 예측 가능해진다.
const name = "Nicolas",
age = 24,
gender = "male";
const sayHi = (name, age, gender) => {
console.log(`Hello ${name}, you are ${age} and a ${gender}`);
}
sayHi(name, age, gender);
export {};
export {}; - 타입스크립트의 법칙으로 파일이 모듈이 된다는 뜻이다.
위의 코드는 평범한 자바스크립트 코드이다.
만약 함수에서 인자를 하나 빼먹으면 ...
타입스크립트는 이렇게 에러가 발생한 것을 알려주며 컴파일이 되지 않는다.
이런 방식으로, 타입스크립트는 우리를 사소한 실수들로부터 보호해준다.
( + 만약 'gender?'처럼 인자 뒤에 ?를 붙이면 선택적이라는 뜻으로 컴파일이 가능하다 )
sayHi function에 마우스 커서를 올려보면, 인자들의 타입이 any로 뜨는 것을 볼 수 있다.
앞서 작성한 코드에서 매개변수들에게 타입을 줘보자.
const sayHi = (name:string, age:number, gender:string): void => {
console.log(`Hello ${name}, you are ${age} and a ${gender}`);
}
sayHi("Nicolas", "444", "male");
export {};
만약 age가 number인데 인자로 string이 들어오면, 위와 같이 에러가 발생하게 된다.
이를 통해 코드가 예측 가능해지며, 실수가 줄어들게 된다.
void는 함수의 타입을 나타낸 것으로, 아무것도 리턴하지 않는다는 것을 의미한다.
tsc-watch
tsc-watch는 타입스크립트의 nodemon 같은 존재로, 변경사항이 있을 시 자동으로 실행을 해준다.
npm install tsc-watch -D
-D는 --save-dev의 약자로, dependencies에 추가한다는 의미이다.
우선, src 폴더와 dist 폴더를 만들어주자.
src 폴더에는 ts 파일을, dist에는 컴파일된 파일들이 들어간다.
tsc-watch를 사용하기 위해 package.json의 scripts-start 파트를 아래와 같이 수정해주자.
그리고 tsconfig.json 파일도 수정해준다.
- outDir: 컴파일된 파일을 dist 폴더에 저장한다
- src/**/*: src 폴더 내 모든 파일을 컴파일한다
이제 npm start를 해보면, 아래와 같이 tsc-watch가 코드의 변경사항을 감시하고 있는 것을 확인할 수 있다.
Interface
만약 우리가 함수의 인자로 객체를 전달해주고 싶을 때, 타입스크립트는 객체를 이해할 수 있어야 하고, 그 객체의 타입도 체크해주어야 한다.
그 방법으로 타입스크립트는 인터페이스를 사용한다.
아래 예제를 통해 이해해보자.
interface Human {
name: string,
age: number,
gender: string
}
const me = {
name: "leeeeeyeon",
age: 21,
gender: "female"
}
const sayHi = (me:Human): void => {
console.log(`Hello ${me.name}, you are ${me.age} and a ${me.gender}`);
}
sayHi(me);
export {};
먼저 interface를 정의해주어 각 변수의 타입을 지정해준다. 그리고 me의 타입을 Human으로 해주어, me가 Human 인터페이스와 같은 구조를 갖고 있는지 체크한다.
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("leeeeeyeon", 21, "female");
const sayHi = (me:Human): void => {
console.log(`Hello ${me.name}, you are ${me.age} and a ${me.gender}`);
}
sayHi(me);
export {};
public 속성으로 각 변수들의 타입을 지정해주고, 생성자(constructor)을 이용해서 변수들에 값을 대입한다.
그리고 클래스를 사용할 때는 new 키워드를 사용하면 된다.
인터페이스가 ts 측면에서는 더 안전하지만, 리액트나 노드 등 다른 라이브러리/프레임워크와 함께 사용할 때 클래스가 필요한 상황이 발생할 수 있다.
+ 위에선 속성을 public으로 했는데, private으로 설정하면 객체지향프로그래밍에서 배웠듯이 클래스 밖에서 사용할 수 없게 된다.
블록체인 생성
이 강의는 블록체인을 만드는 것을 통해 타입스크립트를 학습한다.
지금부터 블록체인을 만들면서 타입스크립트를 실전에서 사용해보고, 더 배워보자.
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, "anything", "", "first block", 12345);
let blockchain: Block[] = [genesisBlock];
console.log(blockchain);
export {};
- Block 클래스 생성
- 첫 번째 block인 genesisBlock 생성
블록체인은 블록의 연결, 즉, 블록들의 배열이다. 그러므로 blockchain의 타입을 [Block]으로 해주자.
타입을 지정해주었으므로, push 함수 등으로 string, number 등 다른 타입의 데이터가 들어올 수 없고, 오직 Block 타입의 변수만 들어올 수 있게 된다.
첫 블록을 만들었으니, 이제 다음 블록을 만들어보자.
새로운 블록을 만들기 위해서는 hash를 계산해야한다.
Hash는 모든 속성을 엄청 길고 수학적으로 하나의 문자열로 결합한 것이다.
Hash를 생성하기 위해 crypto-js를 설치하자.
npm install crypto-js -D
이제 crypto-js를 import를 할건데, import 과정이 살짝 다르다.
import * as CryptoJs from "crypto-js";
Hash를 계산하는 함수는 아래와 같다. Block 클래스 안에서 static method로 만들어주었다.
static이기 때문에, method가 클래스 안에 있지만 클래스를 생성하지 않아도 사용 가능하다.
static calculateBlockHash = (
index:number,
previousHash:string,
data: string,
timeStamp: number):string =>
CryptoJs.SHA256(index + previousHash + data + timeStamp).toString();
그리고 아래와 같이 앞으로 필요할 함수들을 미리 만들어두었다.
const getBlockchain = () : Block[] => blockchain;
const getLatestBlock = () : Block => blockchain[blockchain.length - 1];
const getNewTimeStamp = () : number => Math.round(new Date().getTime() / 1000);
위 함수들을 이용하여 새로운 블록을 만드는 함수도 만들자.
const createNewBlock = (data:string) : Block => {
const previousBlock:Block = getLatestBlock();
const newIndex:number = previousBlock.index + 1;
const newTimeStamp:number = getNewTimeStamp();
const newHash:string = Block.calculateBlockHash(newIndex, previousBlock.hash, newTimeStamp, data);
const newBlock : Block = new Block(newIndex, newHash, previousBlock.hash, data, newTimeStamp);
return newBlock;
}
이제 블록들이 isValid 구조를 가지는지 확인하도록 해보자.
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;
}
}
- 현재 블록의 각 인자들의 타입이 올바른지
- 현재 블록의 index가 이전 블록 index+1인지
- 이전 블록의 hash와 현재 블록에 저장된 이전 블록 hash 값이 일치하는지
- 현재 블록의 hash 값이 올바른지
를 확인한다. 함수 속에서 사용된 validateStructure 함수와 getHashforBlock 함수는 아래와 같다.
static validateStructure = (aBlock:Block) : boolean =>
typeof aBlock.index === "number" &&
typeof aBlock.hash === "string" &&
typeof aBlock.previousHash === "string" &&
typeof aBlock.timeStamp === "number" &&
typeof aBlock.data === "string";
const getHashforBlock = (aBlock:Block) : string => Block.calculateBlockHash(
aBlock.index, aBlock.previousHash, aBlock.timeStamp, aBlock.data);
함께 작성했긴 했지만, validateStructure 함수는 클래스 안에서, getHashforBlock 함수는 클래스 밖에서 작성해주었다.
const addBlock = (candidateBlock:Block) : void => {
if(isBlockValid(candidateBlock, getLatestBlock())){
blockchain.push(candidateBlock);
}
}
createNewBlock("2nd block");
createNewBlock("3rd block");
createNewBlock("4th block");
console.log(blockchain);
addBlock 함수를 만들어 blockchain array에 생성한 블록들을 추가해주고, createNewBlock 함수 안에 addBlock 함수를 추가해주었다.
블록들을 몇 개 추가해주고 블록체인을 출력해보면,
잘 동작하는 것을 확인할 수 있다 ~!~!
[ 전체 코드 ]
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,
timeStamp: number,
data: string):string => CryptoJs.SHA256(index + previousHash + data + timeStamp).toString();
static validateStructure = (aBlock:Block) : boolean =>
typeof aBlock.index === "number" &&
typeof aBlock.hash === "string" &&
typeof aBlock.previousHash === "string" &&
typeof aBlock.timeStamp === "number" &&
typeof aBlock.data === "string";
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, "anything", "", "first block", 12345);
let blockchain:Block[] = [genesisBlock];
const getBlockchain = () : Block[] => blockchain;
const getLatestBlock = () : Block => blockchain[blockchain.length - 1];
const getNewTimeStamp = () : number => Math.round(new Date().getTime() / 1000);
const createNewBlock = (data:string) : Block => {
const previousBlock:Block = getLatestBlock();
const newIndex:number = previousBlock.index + 1;
const newTimeStamp:number = getNewTimeStamp();
const newHash:string = Block.calculateBlockHash(newIndex, previousBlock.hash, newTimeStamp, data);
const newBlock : Block = new Block(newIndex, newHash, previousBlock.hash, data, newTimeStamp);
addBlock(newBlock);
return newBlock;
}
const getHashforBlock = (aBlock:Block) : string => Block.calculateBlockHash(
aBlock.index, aBlock.previousHash, aBlock.timeStamp, aBlock.data);
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);
}
}
createNewBlock("2nd block");
createNewBlock("3rd block");
createNewBlock("4th block");
console.log(blockchain);
export {};
참고
'Javascript' 카테고리의 다른 글
게시판 기능 리팩토링 (0) | 2021.08.04 |
---|---|
게시판 CRUD 기능 만들기 (0) | 2021.08.01 |
React + Node.js 연동 (0) | 2021.07.29 |