Git으로 프로젝트 생성하는 방법

Git으로 프로젝트 생성하여 다른 사람이 작업을 시작할 수 있게 하기 위한 초기 세팅 과정


1. Github/Gitlab 등에서 Repository 생성


Github/Gitlab 등에서 README.md 파일을 포함하여 Repository를 생성한다.


2. 로컬에서 clone 하기


로컬에서 git clone https://github.com/cheonmro/testgit.git 명령어를 실행하여, 원격 저장소를 clone 한다.


3. clone 한 폴더 안에서 프로젝트 폴더를 생성


리액트 프로젝트의 경우, create-react-app testgit 와 같이 새로운 프로젝트 폴더를 생성한다.


4. 원격으로 push 하기(master 브랜치 셋업)


다음과 같이, 원격 저장소로 로컬에 새로 생성한 프로젝트 폴더를 push한다.

git add .

git commit -m "add create-react-app

git push origin master

이렇게 하면, 원격 저장소와 로컬 저장소에 기본적인 리액트 프로젝트 폴더가 들어가게 세팅이 된다.

그러나, 실제 프로젝트 개발시에는 dev 브랜치를 이용해서 하기 때문에, master 브랜치는 여기까지만 생성하고, dev 브랜치를 이용해야 한다.


5. 로컬에서 dev 브랜치 생성


로컬에서 다음의 명령어로 dev 브랜치를 생성한다.

git checkout -b dev

checkout-b를 같이 사용하면, dev 브랜치를 생성하면서 동시에 dev 브랜치로 이동하게 된다.


6. 생성한 dev 브랜치에서 프로젝트를 위한 전반적인 폴더 구조 셋업


현재는 create-react-app으로 만들어진 기본적인 구조밖에 없다. 여기서 우리가 만들 프로젝트에 대한 전반적인 구조를 만들어야 한다.


7. 새롭게 셋업된 로컬의 dev 브랜치를 push 하기


새롭게 프로젝트를 만들었다면, 로컬의 dev 브랜치를 원격저장소에 push 해야한다.

git add .

git commit -m "add project-structure

git push origin dev

이렇게 하여, 본격적인 프로젝트를 시작하기 위한 dev 브랜치가 완성되었다. 이제부터는 각 팀원이 git clone을 하여 각 컴퓨터 로컬에 내려받고, 거기서 각자가 맡은 기능별로 브랜치를 새로 생성하여 개발을 시작하면 된다.

How Ethereum Transaction Works

이더리움 트랜잭션 작동원리


  1. EOA(사용자 계정)가 트랜잭션을 발생(발생한 트랜잭션에는 nonce/to/value/gasLimit/gasPrice/data 등이 들어감)
  2. 발생한 트랜잭션을 사용자가 가지고 있는 지갑의 pk로 전자서명(Sign)을 함(전자 서명 암호화 방식)
  3. Sign된 트랜잭션을 이더리움 네트워크의 노드들에게 보냄(트랜잭션을 브로드캐스트)
  4. 노드(채굴자)들은 해당 트랜잭션의 유효성 검증을 함
    1. 해당 트랜잭션이 문법에 맞게 구성되었는지
    2. 사용자의 공개 키를 사용하여 해당 전자 서명이 유효한지
    3. 사용자 어카운트에 있는 Nonce와 맞는지
  5. 해당 트랜잭션이 유효하면, 해당 트랜잭션은 트랜잭션 풀(Tx pool)에 보관됨
  6. 채굴자들은 수수료가 높은 순서대로 트랜잭션을 처리함
  7. 여러 트랜잭션이 블록에 포함되고 블록이 생성되면, 모든 노드들에게 신규 블록이 전달됨(신규 블록 브로드캐스트)


Ethereum API를 활용한 트랜잭션 만드는 방법


Ethereum API를 활용해서 다음과 같은 기능을 만들어 보자.

  • 이더리움 지갑 및 key 생성
  • balance 조회
  • transfer
  • 토큰 정보 가져오기

위 기능들을 React에서 만들어 보자.


1. 이더리움 지갑 및 key 생성


이더리움의 지갑을 만들기 위해서는 이더리움에서 제공하는 ‘ethereumjs-wallet’ 라이브러리를 사용해야 한다.

‘ethereumjs-wallet’ 설치

1
npm install ethereumjs-wallet

리액트에서 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Wallet from 'ethereumjs-wallet';

class MultiWallets extends Component {

constructor(props) {
super(props);
}

generateWallet = () => {
let wallet = Wallet.generate();
let address = '0x' + wallet.getAddress().toString('hex');
let privKey = wallet.getPrivateKey().toString('hex');
let pubKey = wallet.getPublicKey().toString('hex');
}

render() {
return (
<div className="MultiWallets">
<button onClick={() => this.generateWallet()}>Generate a Wallet</button>
</div>
)
}

}

위 코드는 button을 클릭하면, wallet과 wallet의 address, private key, 그리고 public key를 생성할 수 기능이다.

  • generate() 함수를 사용하여, wallet을 생성한다. 생성한 wallet의 getAddress() 함수를 이용하여 주소값을 만드는데, 이더리움은 ‘0x’를 포함한 hex string으로 값을 만든다. 그래서, toString(‘hex’)와 앞에 ‘0x’를 붙여 address 값을 만든다.
  • 생성한 wallet의 getPrivateKey() 함수와 getPublicKey() 함수를 이용해서 private key와 public key를 생성한다.


2. 지갑의 balance 조회


이더리움 지갑의 balance를 조회하기 위해서는 이더리움에서 제공하는 web3 라이브러리를 사용해야 한다.

web3 설치

1
npm install web3

리액트에서 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Wallet from 'ethereumjs-wallet';
import Web3 from 'web3';

class MultiWallets extends Component {

constructor(props) {
super(props);
}

componentWillMount() {
window.web3 = new Web3(new Web3.providers.HttpProvider('https://ropsten.infura.io/v3/API_KEY'))
}

getBalance = () => {
// 여기서 account는 위에서 만든 address이다.
const { account } = this.state;
window.web3.eth.getBalance(account).then(console.log)
window.web3.eth.getBalance('0xBF345977799290F574dB970366CF1712AdCd0632').then(console.log)
}

render() {
return (
<div className="MultiWallets">
<button onClick={() => this.getBalance()}>Get Balance</button>
</div>
)
}

}

위 코드는 버튼을 클릭하면, balance를 조회하는 기능이다.

  • web3를 사용하기 위해서는 우선, new 연산자를 이용해 web3 인스턴스를 생성하고, HttpProvider를 이용해서 이더리움 네트워크와 연결해야 한다.(위 코드는 이더리움의 테스트넷인 Ropsten의 infura API를 사용하여 infura에서 얻은 API_KEY로 노드를 연결했다.)
  • 만약, Private 블록체인으로 연결하고 싶다면, localhost:8545로 연결하면 된다.
  • web3.eth의 getBalance() 함수를 이용해 내가 가진 지갑주소의 balance를 확인할 수 있다.

*infura

  • infura는 개발자가 API KEY만으로도 이더리움의 메인넷 또는 테스트넷에 접근을 가능하게 한다.
  • infura를 로그인하면 API KEY와 ENDPOINT를 제공해준다.
  • web3의 HttpProvider를 사용해서 이더리움 네트워크에 연결할 때, infura에서 제공하는 ENDPOINT를 사용하면 된다.


3. transfer


transfer(송금) 기능을 만들기 위해서는, transaction을 생성하고, 서명하여 전송하면 된다.

리액트에서 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import Wallet from 'ethereumjs-wallet';
import Web3 from 'web3';

class MultiWallets extends Component {

constructor(props) {
super(props);
}

componentWillMount() {
window.web3 = new Web3(new Web3.providers.HttpProvider('https://ropsten.infura.io/v3/API_KEY'))
}

sendTransaction = () => {
const { account } = this.state;
const sendAccount = '0xBF345977799290F574dB970366CF1712AdCd0632';

// 1. check the number of nonce
window.web3.eth.getTransactionCount(sendAccount, (err, txCount) => {

// 2. create transaction
const txObject = {
nonce: window.web3.utils.toHex(txCount),
to: account,
gasLimit: window.web3.utils.toHex(1000000),
gasPrice: window.web3.utils.toHex(window.web3.utils.toWei('10', 'gwei')),
value: window.web3.utils.toHex(window.web3.utils.toWei('0.01', 'ether'))
};

const tx = new Tx(txObject)

// 3. Sign transaction
// a private key of the sendAccount
const privateKey = 'BE43625EA38CF1FA2D09D057F2E0AB36899E28338E2BCB698CFA6F066EDDF04C';
const _privateKey = Buffer.from(privateKey, 'hex');
tx.sign(_privateKey)

// 4. Send transaction
const serializedTx = '0x' + tx.serialize().toString('hex');
window.web3.eth.sendSignedTransaction(serializedTx, function(err, txId) {
console.log('serializedTx:', serializedTx)
if(!err) {
console.log('txId:', txId)
} else {
console.log('err:', err)
}
})

})
}

render() {
return (
<div className="MultiWallets">
<button onClick={() => this.sendTransaction()}>Send Transaction</button>
</div>
)
}

}

위 코드는 버튼을 클릭하면, transaction을 발생시키는 기능이다.

transaction을 발생시키기 위한 단계는 다음과 같다.

  1. nonce 값 구하기
  2. transaction 생성
  3. transaction 서명
  4. transaction 보내기


1. nonce 값 구하기


1
2
3
window.web3.eth.getTransactionCount(sendAccount, (err, txCount) => {

}

getTransactionCount() 함수를 사용하여 nonce값을 구하는데, getTransactionCount() 함수 안에 인자 txCount가 nonce 값이다. nonce값이란, 해당 transaction을 발생시키는 account가 발생시킨 총 transaction의 수를 말한다.


2. transaction 생성


1
2
3
4
5
6
7
8
9
10
11
12
13
import Tx from 'ethereumjs-tx';

// 1. transaction 생성에 필요한 txObject 생성
const txObject = {
nonce: window.web3.utils.toHex(txCount),
to: account,
gasLimit: window.web3.utils.toHex(1000000),
gasPrice: window.web3.utils.toHex(window.web3.utils.toWei('10', 'gwei')),
value: window.web3.utils.toHex(window.web3.utils.toWei('0.01', 'ether'))
};

// 2. 'ethereumjs-tx' 라이브러리를 이용해 tx 인스턴스 생성
const tx = new Tx(txObject)

transaction을 생성하기 위해 필요한 txObject를 만들어야 하는데, 이 txObject에는 위와 같은 값들이 들어간다. web3의 utils에 있는 toHex() 함수를 이용해 값들을 hex 값으로 만들었는데, 위 코드의 값들은 예시로 작성한 것이다.

참고로, toWei() 함수는 인자로 들어간 값을 Wei 단위로 변환한다는 뜻이다. 예를 들어, window.web3.utils.toWei(‘10’, ‘gwei’)이면, 10 gwei 값을 wei 값으로 변환하라는 뜻이다.

위에서 만든 txObject를 가지고 transaction을 생성하기 위해서는 ethereumjs-tx 라이브러리를 사용해 tx를 생성한다.


3. transaction 서명


이더리움에서 transaction을 서명(=== 전자서명)할 때는, 수신자의 private key를 가지고 transaction을 서명한다.

1
2
3
4
// a private key of the sendAccount
const privateKey = 'PK';
const _privateKey = Buffer.from(privateKey, 'hex');
tx.sign(_privateKey)

private key로 서명하기 위해서는 private key 값을 buffer 타입으로 변경하여 사용해야 한다.

buffer 타입이란, Unit8Array 타입으로, 값들이 배열(‘[]’)에 들어가 있다. 처음에 generate() 함수로 wallet을 생성했을 때, 그 wallet 안에 들어있는 private key 값의 타입은 Unit8Array 타입으로 되어있다. 이 private key 값을 위에서 hex 값으로 변경해서 사용했었는데, 이 값을 다시 buffer 타입으로 변경해서 Sign 할 때 사용해야 한다.


4. transaction 보내기


서명된 transaction을 serialize하여 sendSignedTransaction() 함수를 이용해 transaction을 보낸다.

1
2
3
4
5
6
7
8
9
10
11
12
// 1. serialize transaction
const serializedTx = '0x' + tx.serialize().toString('hex');

// 2. send signed transaction
window.web3.eth.sendSignedTransaction(serializedTx, function(err, txId) {
console.log('serializedTx:', serializedTx)
if(!err) {
console.log('txId:', txId)
} else {
console.log('err:', err)
}
})

Serialization이란, 네트워크를 통해 데이터를 주고 받을 때, 서로간에 공유하는 규칙이 있는데, 이 규칙에 맞게 데이터를 출력하는 것을 말한다.

sendSignedTransaction() 함수를 사용해 얻은 txId 값은, 이 transaction의 txHash값이 되고, 즉 transaction id가 된다. 이 txHash값으로 Ether Scan에서 이 transaction에 대한 거래내역을 확인할 수 있다.

참고: Serialization


4. 토큰 정보 가져오기


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Web3 from 'web3';
import { erc20Abi } from '../constant/abi';
import { ethToken } from '../constant/ethToken'

class MultiWallets extends Component {

constructor(props) {
super(props);
}

addToken = () => {
// Omisego token
const tokenAddress = '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07';
let token = window.web3.eth.Contract(erc20Abi, tokenAddress)

// get token info
const tokenInfo = ethToken.filter((token) => {
return token.address.toLowerCase() === tokenAddress.toLowerCase()
});
console.log('tokenInfo:', tokenInfo)
console.log('symbol:', tokenInfo[0].symbol)
console.log('decimal:', tokenInfo[0].decimal)
}


render() {
return (
<div className="MultiWallets">
<button onClick={() => this.addToken()}>Add Token</button>
</div>
)
}


}

위 코드는 addToken 버튼을 클릭하면, 해당 토큰의 정보를 가져올 수 있는 기능이다.

각 토큰은 토큰의 address를 가지고 있다. web3의 Contract() 함수를 사용하면 토큰의 정보를 가져올 수 있다.

1
new web3.eth.Contract(jsonInterface, address, options)

Contract() 함수안에 jsonInterface는 abi 파일로써, contract가 인스턴스화하기에 필요한 json interface 파일이다.(abi 파일)

위 코드의 ethToken는 이더리움의 모든 토큰에 대한 정보를 모아놓은 파일이다. 즉, 선택한 토큰이 이 파일안에 있는 모든 이더리움 토큰과 비교해 해당 토큰을 찾아 그 토큰에 대한 정보를 가져오는 방식으로 코드를 작성했다.

Ethereum State Transition Model(이더리움 상태 전이 모델)

정의


이더리움 네트워크에서는 트랜잭션이 발생할 때마다, 이에 대한 신뢰 여부를 채굴자들의 합의하에 블록을 생성하여 모든 트랜잭션을 처리한다. 이렇게 트랜잭션을 처리하게 되면, 관련된 Account의 상태가 변화하게 된다. 즉, 이더리움은 이와 같이 상태가 변화하는 상태 전이 과정을 기반으로 작동한다.

트랜잭션 발생 -> 블록 생성 -> 관련된 Account의 상태 변화

모든 Account의 상태 정보는 블록과 블록 내에 연결된 머클 패트리시아 트리로 저장되고 관리된다.


상태 전이(State Transition)


상태 전이란, 특정 시점의 현재 상태 S가 상태 전이 함수(APPLY())에 의해 다른 상태(S’)로 전이되거나 전이에 실패하고 이전 상태로 복귀되는 것을 말한다.
Account의 상태는 상태 전이 함수에 의해 전이된다.

APPLY(S, TX) -> S’
[S: 현재 상태] -> (APPLY: 상태 전이 함수) -> [S’: 전이된 상태 또는 실패]

상태 전이 함수

  • 개인 Account간의 송금 등에 의한 트랜잭션
  • 이더리움 가상 머신에서 실행되는 프로그램인 스마트 컨트랙트에 의한 트랜잭션

State Transition
출처: State Transition


이더리움 단일 상태 전이


이더리움의 상태(State)는 복수의 상태 변이를 갖지 못하고, 단 하나의 상태 변이만을 갖는다. 즉, Account의 특정 시점의 한 상태는 상태 전이 함수를 통해 단 하나의 상태(single state)로만 전이된다는 것이다. 만약, 하나의 상태가 여러 개의 상태로 전이된다면, 채굴자들은 어떤 상태가 맞는 것인지 판단하고 합의 할 수 없다.

Ethereum Single State Transition
출처: Ethereum Single State Transition


상태 전이 프로세스


  1. 기본 트랜잭션 형식 확인
  2. Gas 값 수수료 계산 및 잔고 확인
  3. 기본 트랜잭션 형식 확인
    1. 트랜잭션의 형식이 맞는지 확인
    2. 서명이 유효한지 확인
    3. Nonce가 발신처 Account의 Nonce와 일치하는지 확인
    4. 트랜잭션이 유효하지 않을 경우, Error를 반환

Transaction fields
출처: Transaction fields

  1. Gas 값 수수료 및 잔고 확인
    1. 트랜잭션 수수료(=gasLimit * gasPrice) 계산
    2. 서명으로부터 발신처 주소를 결정
    3. 발신처 Account의 balance에서 계산한 트랜잭션 수수료를 빼고, 발신자 Nonce를 증가
    4. 발신처 balance가 충분하지 않으면, Error를 반환
    5. GAS = gasLimit으로 초기화 한 후, 트랜잭션에서 사용된 byte에 대한 값을 지불하기 위해 byte 당 gas의 특정 양을 차감(트랜잭션의 총 byte 길이 * byte 당 gas 수수료)
    6. 발신처 Account에서 수신처 Account로 트랜잭션 값을 보냄
    7. 이때, 수신처 Account가 존재하지 않으면 새로 생성
    8. 만약, 수신처 Account가 컨트랙트면, 컨트랙트 코드를 끝까지 수행
    9. 이때, GAS가 모두 소모되면, 소모될 때까지만 수행
    10. 발신처의 balance가 전송할 값보다 적거나, 코드 수행시 GAS가 부족하면, 모든 상태 변경을 원상태로 돌려놓음
    11. 이때, 코드를 수행한 만큼의 GAS는 채굴자 Account로 더해지고, 나머지 GAS는 발신처 Account에 다시 돌려짐

수수료

  • 트랜잭션 처리에 대한 수수료
  • 스마트 컨트랙트 코드 실행 처리에 대한 수수료


상태 전이 예제


<예제>

  • 트랜잭션은 다음과 같은 field를 가지고 전달된다고 가정
    • Value = 10 ether
    • gasLimit = 2,000
    • gasPrice = 0.001 ether
    • Data = 64 bytes(스마트 컨트랙트 호출에 필요한 parameter 값)
      • 0-31 bytes까지는 숫자 2로 나타내고, 32-63 bytes는 CHARLIE 문자열로 나타냄)
  • 트랜잭션의 총 길이 = 170 bytes, byte 당 GAS 수수료 = 5 gas (트랜잭션 처리에 대한 수수료 = 170 * 5 = 850 GAS)
  • 스마트 컨트랙트 코드 실행시 GAS 수수료 = 187 GAS


<프로세스>

  1. 트랜잭션의 형식과 서명이 유효한지 확인(기본 트랜잭션 형식이 맞는지 확인)
  2. 트랜잭션 발송처가 최소 2000 * 0.001 = 2 ether를 가지고 있는지 확인하고, 발송처의 Account에서 2 ether를 차감
  3. 발신처 balance가 2 ether 보다 적으면, Error를 반환
  4. 발신자 Nonce를 증가
  5. GAS = 2,000으로 초기화 한 후, 트랜잭션 처리에 대한 수수료 850 GAS를 차감하면, 1150 GAS가 남음
  6. 발신처 Account에서 보내고자 하는 10 ether를 차감하고, 이것을 수신처 Account에 보냄
  7. 이때, 수신처 Account가 스마트 컨트랙트면, 컨트랙트 Account에 더함
  8. 스마트 컨트랙트의 코드를 실행하여 스마트 컨트랙트의 코드 실행 처리 수수료인 187 GAS를 차감하면, 963 GAS가 남음(1150 - 187 = 963)
  9. 963 * 0.001 = 0.963 ether를 송신처의 Account에 되돌려주고, 상태를 반환
  10. 만약, 코드 수행시에 GAS가 모두 소모되면, 소모될 때까지만 수행
  11. 코드 수행시, GAS가 부족하면, 모든 상태 변경을 원상태로 돌려놓음
  12. 이때, 코드를 수행한 만큼의 GAS는 채굴자 Account로 더해지고, 나머지 GAS는 발신처 Account에 다시 돌려짐

State Transition Example
출처: State Transition Example

ICON Data

ICONex에서 Transfer 할 때, 입력할 수 있는 Data 타입


ICONex에서 송금할 때, data를 입력할 수 있다. 여기서 말하는 data는 message를 말하는데, 전달할 message가 있다면 작성할 수 있고, 기본적으로 optional이다.

입력하는 data의 타입은 UTF-8HEX 2가지다.


UTF-8


  • UTF-8이란, 어떤 언어의 문자든 유니코드(unicode)로 인코딩하는 방식으로, 여러 방식이 있으나 이 방식을 가장 많이 사용한다.
    • 유니코드(unicode)란, 전 세계의 모든 문자를 컴퓨터에서 일관되게 표현하는 표준 코드를 말한다.
    • 인코딩이란, ‘부호화하다’라는 의미로, 이를 컴퓨터 공학에서 정의하면, 입력 데이터를 컴퓨터속에서 사용하는 코드로 변환하는 것을 말한다.
      • 즉, 문자나 기호들의 집합을 컴퓨터에 저장하거나 통신에 사용할 목적으로 부호화하는 방법이다.
  • 디코딩이란, ‘복호화하다’라는 의미로, 부호화(Encoding)된 정보를 부호화되기 전으로 되돌리는 처리 방식을 말한다.


정보를 표현하기 위한 글자들의 집합을 문자집합(Character Set)이라고 하고, 다음의 2가지 종류가 대표적인 표준코드이다.

  • ASCII코드
  • 유니코드

ASCII코드는 영문 알파벳을 사용하는 대표적인 문자 인코딩된 표준 코드이다. 그러나, 한글 등 다른 모든 언어를 사용하기 위해서는 이 방식보다 다른 방식을 더 사용한다. 그것은 바로, 유니코드이다.

유니코드란, 전 세계의 모든 문자를 컴퓨터에서 일관되게 표현하고 다룰 수 있도록 설계된 산업 표준코드이다.

내가 제작하려고 하는 웹애플리케이션 또는 웹페이지에서 다국어를 지원하기 위해서는 사용된 문자를 유니코드로 인코딩 해야하고, 이때 인코딩하는 방식 중 UTF-8를 주로 사용한다. UTF-8를 사용하면, 전 세계 모든 문자를 표현할 수 있고, 한 문자를 표현하기 위해 1바이트에서 4바이트까지 사용한다.

웹 개발을 할 때, html에서 다음과 같이 작성한다.

1
2
3
<head>
<meta charset=UTF-8" />
</head>

위와 같이 작성하면, html 문서의 문자는 UTF-8 방식으로 인코딩하여 유니코드로 변환한다는 뜻이다. 만약, 위와 같이 작성하지 않으면, html 문서를 브라우저에서 구현할 때 한글 등 다른 언어들이 깨져 보일 수가 있다.

참고:


HEX


HEX란, 0x를 접두어(prefix)로 사용한 16진수를 표현하는 방식으로, Hexadecimal의 줄임말이다.

예를 들어, 0x547261636b65722063656c6c20ed858cec8aa 와 같이 표현한다.


Data 입력시 수수료 부과


ICONex에서 송금시에 트랜잭션에 사용될 기본적인 Step Limit이 default로 있고, Step Price는 고정되어 있다. 이 둘을 곱한 값이 총 수수료이다.

그런데, 만약 Data를 입력하게 되면, 입력한 Data의 양만큼의 추가 수수료가 부과된다. 이는 Data를 입력하는 양에 따른 Step Limit이 변경되어 최종 수수료가 변경되기 때문이다.

ICON Unit

Ether의 기본단위


이더리움의 가장 작은 단위는 wei 이다.

Name Description
1 ether 110^18 wei(=== 1e^18 wei)
1 ether 110^9 Gwei(=== 1e^9 Gwei)
1 Gwei 110^9 wei(=== 1e^9 wei)

참고로, Gwei는 가스(이더리움 네트워크 거래 수수료) 비용을 계산할 때, 주로 사용한다.


ICX의 기본단위


ICON도 이더리움과 비슷한 개념을 사용한다.

ICON의 가장 작은 단위는 loop 이다.

Name Description
1 icx 10^18 loop(=== 1*e^18 loop)
1 icx 10^9 Gloop(=== 1*e^9 Gloop)
1 Gloop 10^9 loop(=== 1*e^9 loop)

참고로, Gloop는 step(ICON 네트워크 거래 수수료) 비용을 계산할 때, 주로 사용한다.


Ethereum Fee System


이더리움에서 어떤 1개의 트랜잭션을 실행하는 데 사용자가 지불해야하는 수수료를 최대 트랜잭션 실행 비용(Max Transaction Fee)이라고 한다.

Max Transaction Fee(최대 트랜잭션 실행 비용) = Gas Limit(가스 총량) * Gas Price(가스 가격)

Gas Limit(가스 총량)이란, 트랜잭션을 실행하는데 예상되는(추정되는) 총 가스량을 말한다.

Gas Price(가스 가격)이란, 1 가스당 가격을 말한다.

즉, 어떤 사용자가 1개의 트랜잭션을 수행하려고 하면, 그 트랜잭션을 수행하는 것에 대한 수수료(최대 트랜잭션 실행 비용)를 지불해야 하는데, 그 수수료는 Gas Limit(가스 총량) * Gas Price(가스 가격)으로 계산되어진다.

이때, Gas Limit(가스 총량)은 각 트랜잭션에 필요한 총 가스량을 추정(estimated)해서 계산된 값이 나오고, Gas Price(가스 가격)은 1 가스당 가격으로 이 가격이 높을수록 마이너들이 보상을 많이 받기 때문에, 트랜잭션이 빨리 처리가 된다. 즉, 트랜잭션 실행 속도가 빠르게 된다.


How Gas works
출처: How Gas works


어떤 트랜잭션을 실행하려면, 사용자 어카운트에 있는 ether 잔액이 최대 트랜잭션 실행 비용보다 많아야 한다.

  • 만약 적거나, 트랜잭션 실행 도중에 가스를 모두 사용하면, 해당 트랜잭션은 중단되고 이전 상태로 복귀되지만, 사용된 가스는 발신자에게 반환되지 않는다.
  • 만약 ether 잔액이 충분하고, 트랜잭션이 성공적으로 실행이 되었는데, 가스가 남았다면 이 가스는 사용자에게 반납된다.


ICON Fee System


ICON의 step limit과 step price는 이더리움의 gas limit과 gas price 개념과 동일하다.

Estimated Transaction Fee(최대 트랜잭션 실행 비용) = Step Limit(스텝 한도) * Step Price(스텝 가격)

Step Limit(스텝 한도)이란, 트랜잭션을 실행하는데 예상되는(추정되는) 총 스텝양을 말한다.

Step Price(스텝 가격)이란, 1 스텝당 가격을 말한다.

즉, 어떤 사용자가 1개의 트랜잭션을 수행하려고 하면, 그 트랜잭션을 수행하는 것에 대한 수수료(최대 트랜잭션 실행 비용)를 지불해야 하는데, 그 수수료는 Step Limit(스텝 한도) * Step Price(스텝 가격)으로 계산되어진다.

ICON에서 송금 등의 트랜잭션 실행을 할 때, 기본적으로 Step Limit이 100000이다. 이 값은 default로 사용자가 Step Limit의 값을 변경할 수 있다. 그러나, Step Limit의 최대값과 최소값은 정해져 있어서 그 범위안에서만 변경이 가능하다.

Step Price는 0.00000001 ICX (10 Gloop)로 고정되어있다.

어떤 트랜잭션을 실행하려면, 사용자 어카운트에 있는 icx 잔액이 최대 트랜잭션 실행 비용보다 많아야 한다.

  • 만약 적거나, 트랜잭션 실행 도중에 step을 모두 사용하면, 해당 트랜잭션은 중단되고 이전 상태로 복귀되지만, 사용된 step은 발신자에게 반환되지 않는다.
  • 만약 step limit을 잔액보다 높게 설정하더라도, 해당 트랜잭션에 필요한 step 만큼만 소진된다.

Redux-Saga

Redux-Saga란 무엇인가?


Redux-saga란, side effect를 관리하기 위한 middleware이다.

Side effect란, 애플리케이션의 부작용들(데이터 요청 등의 비동기 작업/브라우저 캐시 같은 순수하지 않은 것들)을 말한다.

middleware란, 액션이 dispatch 되어서 리듀서에서 이를 처리하기 전에 그 사이에서 중간자로써 지정된 특정 작업을 하는 것을 말한다.


Redux-Saga를 왜 사용하는가?


비동기 처리 등 순수하게 액션만을 전달하는 것 이상으로 발생하는 상황들(side effects)을 다룰 때, 이 미들웨어를 사용한다.


Redux-Thunk 대신에 Redux-Saga를 사용하는 이유는 무엇인가?


redux-thunk를 사용하면, 비동기 처리를 Action Creator(액션 생성자 함수)에서 작성하게 되어, 로직이 복잡해진다. 기본적으로 Redux에서 Action Creator에서는 액션을 객체형태로 생성하여 dispatch 하는데, redux-thunk에서는 액션을 함수형태로 생성하여 dispatch 하여, Action Creator에 비동기처리 코드나 관련된 로직이 들어가게 된다.

그런데, Redux-saga는 비동기 처리를 기술하는 전용의 방식인 task로 쓰여있다. 즉, 비동기 처리를 각 saga(task)에서 명령을 내리고, 동작을 미들웨어에서 처리한다. 그래서, redux에서의 action creator에서 기존과 그대로 액션을 객체형태로 생성하여 dispatch 할 수가 있고, saga에서의 코드도 더 간단해진다. 결국, Action Creator는 본래의 모습을 되찾아, action 객체를 생성하여 돌려주는 순수한 상태로 돌아가게 된다.

결론적으로, Redux-Saga를 사용하게 되면, 비동기 처리를 액션 생성자로부터 분리하여 작성하기 때문에, 가독성도 좋고 콜백 처리 등에 더 수월하다.


Redux-Saga Architecture


Redux-Saga Architecture
출처: Redux-Saga Architecture


Redux-Saga Flow


Redux-Saga Architecture
출처: Redux-Saga Architecture)

위 Flow를 정리하면 다음과 같다.

  1. 어떤 비동기 액션이 dispatch 되면, Reducer에 먼저 도달한다. 정확하게는 액션이 Reducer로 지나가는 것을 본 후, Redux-Saga에 액션을 처리한다. Reducer는 순수 함수라는 규칙이 있기 때문에, 비동기 액션을 처리하지 못한다.
  1. 이 비동기 액션을 Redux-Saga에 있는 watcher saga가 보고 있다가, watcher saga에 등록되어 있는 task를 수행한다. 이 watcher saga의 역할은 어떤 비동기 액션이 dispatch 되면, 어떤 task를 수행하도록 등록하는 것이다. 이때, takeEvery라는 헬퍼 이펙트를 사용하는데, 이 이펙트는 여러개의 task를 동시에 시작할 수 있다. 즉, 1개 혹은 아직 종료되지 않은 task가 있더라도 새로운 task를 시작할 수 있다.
1
2
3
4
// INCREMENT_ASYNC 액션이 Dispatch 되면 `incrementAsync`를 수행하도록 등록한다.
export function* watchIncrementAsync() {
yield takeEvery(INCREMENT_ASYNC, incrementAsync)
}
  1. incrementAsync를 수행하는데, 만약 task가 2초 마다 +1씩 증가하는 함수라고 한다면, 다음과 같이 작성한다.
1
2
3
4
function* incrementAsync(action) {
yield delay(2000) // 2초를 기다리고
yield put({ type: INCREMENT }) // INCREMENT 액션을 Dispatch한다.
}
  1. 위 task가 수행될 때, 먼저 첫번째 줄에 있는 delay(2000)이 yield 된다. delay는 설정된 시간 이후에 resolve를 하는 Promise 객체를 리턴하는 함수이다. 이 Promise가 미들웨어에 yield 될때, 미들웨어는 Promise가 끝날때까지 Saga를 일시정지 시킨다.(generator 함수의 특징) 즉, 이 부분은 동기적으로 동작한다.
  1. 2초후, Promise가 한번 resolve 되면, 미들웨어는 saga(task)를 다시 작동시키면서, 다음 yield까지 코드를 실행한다. 이런 방식으로 saga(task)에서 1개씩 yield되고, 그 yield 된 것이 미들웨어에 의해 동작이 완료되면, 그때 그 다음줄에 있는 객체가 yield된다.
  1. 어떤 객체를 yield할때, 앞에 오는 것들(put, call 등)을 이펙트라고 한다. 이펙트란, 미들웨어에 의해 수행되는 명령을 담고있는 간단한 자바스크립트 객체를 말한다. 미들웨어가 saga에 의해 yield 된 이펙트를 받을때, saga는 이펙트가 수행될때까지 정지되어 있다.
  1. saga에서 put을 통해 객체를 dispatch하면, 이 객체는 reducer로 가게된다.
  1. 두 saga를 모두 한번에 실행하게 해주기 위해, rootSaga를 이용해 사용한다.
1
2
3
4
5
6
7
// 모든 Saga들을 한번에 시작하기 위한 단일 entry point 이다.
export default function* rootSaga() {
yield all([
incrementAsync(),
watchIncrementAsync()
])
}


Redux-Saga 사용방법


Redux-Saga를 사용하는 방법은 다음과 같다.

  • saga 미들웨어 작성(sagas.js)
    • 비동기 처리를 위한 task 작성(제너레이터 함수) - worker saga
    • 각각의 어떤 비동기 액션을 처리하기 위해 watch 함수를 작성하고, 그 안에 takeEvery 이펙트 펠어 함수를 사용 - watcher saga
    • 모든 saga들을 한번에 실행하기 entry point 작성 - rootSaga 작성
      • rootSaga에 들어있는 두 saga가 호출된 결과의 배열을 yield 한다. 이것은 생성된 두 제너레이터가 병렬로 시작된다는 것을 의미.
  • Saga 미들웨어를 Redux 스토어에 연결(main.js)
    • applyMiddleware
    • sagaMiddleware.run 등 작성
  • 비동기 호출을 위한 이벤트 등록(Counter.js)

saga 미들웨어 작성(sagas.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// sagas.js

import { delay } from 'redux-saga'
import { put, takeEvery } from 'redux-saga/effects'

// watcher saga
export function* watchIncrementAsync() {
yield takeEvery(INCREMENT_ASYNC, incrementAsync)
}

// worker saga
function* incrementAsync(action) {
yield delay(2000)
yield put({ type: INCREMENT })
}

// rootSaga
export default function* rootSaga() {
yield all([
incrementAsync(),
watchIncrementAsync()
])
}

Saga 미들웨어를 Redux 스토어에 연결(main.js)

1
2
3
4
5
6
7
8
9
// main.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = ...
sagaMiddleware.run(rootSaga)

비동기 호출을 위한 이벤트 등록(Counter.js)

1
2
3
4
5
6
7
8
9
10
11
// Counter.js
const Counter = ({ value, onIncrementAsync }) =>
<div>
<button onClick={onIncrementAsync}>
Increment after 2 second
</button>
<hr />
<div>
Counter: {value} times
</div>
</div>

비동기 호출을 처리하기 위해 Redux와 연결해주는 컨테이너 컴포넌트(CounterContainer.js)

1
2
3
4
5
6
7
8
function render() {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrementAsync={() => action('INCREMENT_ASYNC')}
/>
)
}

Redux

Redux란 무엇인가?


Redux란, 상태관리에 대한 라이브러리로써, 애플리케이션의 모든 컴포넌트에 대한 중앙집중식 저장소 역할을 하며, 단순히 하나의 컴포넌트가 아닌 여러 개의 컴포넌트에 적용하는 상태들을 관리하는 곳이다.


Redux를 왜 사용하는가?


리액트에서 기본적으로 부모 컴포넌트에서 상태를 관리하고, 자식으로 상태를 전달한다. 그러나, 앱 규모가 커지면, 데이터가 많고, 유지보수가 힘들다. Redux를 사용하면, 상태값을 컴포넌트에 종속시키지 않고, 상태 관리를 컴포넌트 바깥에서 관리가 가능하다.


Redux 필수 개념


  • Action

    • Action이란, 상태에 변화를 일으킬 때 참조할 수 있는 객체이다.
    • Action 객체는 필수적으로 type 이라는 값을 가지고 있어야 한다.
    • 예로, { type: ‘INCREMENT’ } 라는 객체를 전달 받으면, Redux Store는 ‘상태에 값을 더해줘야 하구나’라고 생각하고 Action을 참조하게 된다.
    • type은 필수이고, 그 다음 값(예로 diff 등)은 option이다.
  • Reducer

    • Reducer란, 액션 객체를 받으면 액션의 타입에 따라 어떻게 상태를 업데이트 하는지, 업데이트 로직을 정의하는 함수이다.
    • Redux Store에는 Reducer가 있다.
    • Reducer에는 2개의 parameter가 존재한다.
      • State: 현재 상태
      • Action: 액션 객체
    • 상태가 변하면, 구독하고 있던 컴포넌트에게 알려서, 새로운 상태로 리렌더링함.


Redux 3가지 규칙


  • 각 애플리케이션에서는 단 1개의 Store를 사용한다.

  • 상태는 읽기전용이다.

    • 기존의 상태는 건들이지 않고, 새로운 상태를 생성하여 업데이트 해주는 방식으로 하면, 나중에 개발자 도구를 통해서 뒤로 돌릴 수도 있고, 다시 앞으로 돌릴 수도 있다.
    • immutable.js를 사용하여 불변성을 유지하며 상태를 관리한다.
  • 변화를 일으키는 함수, 즉, Reducer는 순수한 함수이다.

    • 순수한 함수란, 동일한 인풋이라면 언제나 동일한 아웃풋이 나오는 함수이다. 즉, 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야 한다.
    • 이전 상태는 안 건들이고, 변화를 일으킨 새로운 상태 객체를 만들어서 반환한다.
    • 순수 하지 않은 것, 즉 실행 할 때마다 다른 결과값이 나오는 것들은 Reducer 함수가 아닌 바깥에서 처리해야 한다.
      • 실행할 때마다 다른 결과값이 나오는 것들
        • New Date()
        • 랜덤 숫자 생성
        • 네트워크에 요청


Redux 파일 구조


  • Root.js - React 앱에 Redux 적용(최상위 컴포넌트)
  • 컨테이너 컴포넌트(Store)
    • CounterContainer.js - Redux와 연동하는 컴포넌트
  • 프레젠테이셔널 컴포넌트(View)
    • Counter.js - Counter 기능을 보여주는 View
  • Store
    • modules - 기능별로 액션 및 리듀서를 각 1개 파일에 작성(하나의 파일에 모두 작성하는 것을 Ducks 구조라고 한다.)
      • counter.js - counter 기능과 관련된 액션 및 리듀서
      • index.js - 리듀서 합치는 파일
    • configure.js - Redux 스토어를 생성하는 함수를 모듈화하여 내보내는 파일
    • index.js - store를 생성하여 모듈화하여 내보내는 파일
      • 이렇게 모듈화된 스토어는 브라우저 에서만 사용되는 스토어
      • 서버사이드 렌더링을 하면, configure를 통하여 그때그때 만든다.
      • 이렇게 모듈화된 스토어는 리액트 app을 초기설정할 때 사용
    • actionCreators.js
      • 스토어를 불러오고, 각 모듈들에서 선언했던 액션 생성함수들을 불러와서 스토어의 dispatch와 미리 바인딩 작업을 한다.


Redux Flow


Redux Flow
출처: Redux Flow


Redux 사용법


  • Redux install: npm install redux react-redux
  • 리액트 앱에 Redux 적용하기(Root.js)
    • 리액트 앱에 Redux를 적용할 때는, react-redux에 있는 Provider를 사용
  • 기능을 보여줄 화면 구성(Counter.js)
    • Counter 숫자화면
      • 증가/감소 버튼
  • 기능과 관련된 action과 reducer 작성(modules/counter.js)
    • 액션 타입 정의
    • 액션 생성 함수(ActionCreator) -> createAction 사용
    • 모듈의 초기 상태 정의
    • Reducer 정의: switch문 -> handleActions 사용
  • 여러 reducer가 있을 경우, 하나로 합치기(modules/index.js)
  • store를 만드는 함수 configure 만들어서 내보내기(store/configure.js)
    • Const store = createStore(modules)
  • 위에서 만든 configure 함수를 사용하여 스토어를 만들고 내보내기(store/index.js)
  • Redux와 연동할 컨테이너 컴포넌트 만들고, 그 안에 프레젠테이셔널 컴포넌트인 counter.js를 리턴해서 화면에 보여주기(containers/CounterContainer.js)
  • 이 컴포넌트를 App에 불러와 화면에 보여주기(components/App.js)
  • 컨테이너 컴포넌트를 Redux에 연결하기(containers/CounterContainer.js)
    • mapStateToProps: props 값으로 넣어 줄 상태를 정의
      • 컴포넌트에 state로 넣어줄 props를 반환
      • 컴포넌트에 넣어줄 액션 함수들을 반환
    • mapDispatchToProps: props 값으로 넣어 줄 액션 함수들을 정의
    • Connect(): 컴포넌트를 Redux와 연동 할 때 사용


Redux Architecture


Redux Architecture
출처: Redux Architecture


실제 코드를 작성할 때, 위 그림을 연상하면서 코드를 짜면 좀 더 쉬울 것이다.


Redux 코드 예제


[1] npm으로 redux를 설치한다.

1
npm install redux react-redux


[2] 리액트 앱에 Redux를 적용한다.

이때, react-redux에 있는 Provider를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { store } from 'redux/store/store';
import { Provider } from 'react-redux';
import App from 'app/App.js';

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root'));

//registerServiceWorker();


[3] 보여줄 화면 UI를 만든다.

“KR” 또는 “EN”을 클릭하면, 각 언어에 맞게 화면이 바뀌는 화면 컴포넌트다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Header.js

import React, { Component } from 'react';
import {
routeConstants as ROUTE
} from 'constants/index.js';
import { Link, withRouter } from 'react-router-dom';
import withLanguageProps from 'HOC/withLanguageProps';
import { isEmpty } from 'utils';

const INIT_STATE = {
showInfo: false,
toggleLanguageList: false
}

@withRouter
@withLanguageProps
class Header extends Component {

constructor(props) {
super(props);
this.state = INIT_STATE;
}


render() {
const { setLanguage, language, location, I18n } = this.props;

return (
<div className="header-wrap" onClick={this.onHeaderClick}>
<div className="wrap-holder">
{showHeaderItem && (
<Link to={ROUTE['home']}><p className="logo"><span className="_img"></span></p></Link>
)}

// "KR" & "EN" 클릭 View
<div className="language-holder">
<span onClick={() => setLanguage('kr')} className={language === 'kr' ? 'on' : ''}>KR</span>
<span className="dot">·</span>
<span onClick={() => setLanguage('en')} className={language === 'en' ? 'on' : ''}>EN</span>
</div>

</div>
</div>
);
}
}

export default Header;


[4] 액션타입과 액션생성자 함수를 만든다.

액션타입은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
actionTypes.js

const actionTypes = {

// globalActions
setLanguage: 'SET_LANGUAGE',

}

export default actionTypes;

액션생성자 함수는 다음과 같다.

1
2
3
4
5
6
7
8
import actionTypes from 'redux/actionTypes/actionTypes';

export function setLanguage(lan) {
return {
type: actionTypes.setLanguage,
payload: lan
};
}


[5] Reducer 함수를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
globalReducer.js

import actionTypes from 'redux/actionTypes/actionTypes';

const initialState = {
language: 'en',
}

export function globalReducer(state = initialState, action) {
switch (action.type) {
case actionTypes.setLanguage:
return Object.assign({}, state, {
language: action.payload
})

default:
return state
}
}


[6] store를 만드는 함수를 만들고, 내보낸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
store.js

import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from 'redux/reducers/rootReducer';
import rootSaga from 'redux/sagas/rootSaga';
import persistState from 'redux-localstorage';
import { composeWithDevTools } from 'remote-redux-devtools';

const generateStore = (port) => {
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = composeWithDevTools({ realtime: true, hostname: 'localhost', port: port });
const composeFunc = process.env.NODE_ENV === 'production' ? compose : composeEnhancers;
const store = createStore(rootReducer, {}, composeFunc(
applyMiddleware(sagaMiddleware),
persistState(null, {
slicer: (paths) => (state) => {
return {
global: state['global'],
}
}
}),
)
);
sagaMiddleware.run(rootSaga);
return store
}

const store = generateStore(8000);

export {
store
}


[7] rootReducer를 만든다.

rootReducer는 모든 Reducer들을 한번에 모아놓은 파일이다.

1
2
3
4
5
6
7
8
9
10
11
import { combineReducers } from 'redux';
import { globalReducer } from 'redux/reducers/globalReducer'
import { signupReducer } from 'redux/reducers/signupReducer'


const rootReducer = combineReducers({
global: globalReducer,
signup: signupReducer,
});

export default rootReducer;


[8] store.js에서 store 생성후, index.js의 Provider에 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { store } from 'redux/store/store';
import { Provider } from 'react-redux';
import App from 'app/App.js';

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root'));


[9] Container 컴포넌트를 Redux와 연결한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
HeaderContainer.js

import { connect } from 'react-redux';
import { Header } from 'app/components/';
import { setLanguage } from 'redux/actions/globalActions';
import { withRouter } from 'react-router-dom';

// props 값으로 넣어 줄 상태를 정의
function mapStateToProps(state) {
return {
wallets: state.wallet.wallets,
};
}

// props 값으로 넣어 줄 액션함수들을 정의
function mapDispatchToProps(dispatch) {
return {
setLanguage: (lan) => dispatch(setLanguage(lan))
};
}

// 컴포넌트와 Redux를 연동할 때 connect() 사용
const HeaderContainer = connect(mapStateToProps, mapDispatchToProps)(Header);

export default withRouter(HeaderContainer);


Web Worker

Web Worker란 무엇인가?


Web Worker란, script 실행을 main thread(UI thread)가 아니라 background thread에서 실행할 수 있도록 해주는 API이다.

*thread란, 어떤 프로그램 내에서 실행되는 흐름의 단위로, 보통 한 프로그램이 하나의 thread를 가지고 있다. 하나의 thread를 갖는다는 의미는, 어떤 이벤트(행위)가 발생했을 때, 그 하나의 이벤트를 처리할 때까지 다른 일은 못하게 된다는 뜻이다. 즉, 한번에 하나씩 처리한다는 뜻이다.

둘 이상의 thread를 동시에 실행하는 방식을 multi thread라고 한다. 웹 브라우저상의 javascript는 싱글 thread이기 때문에, 한번에 하나의 일 밖에 처리를 못한다. 그래서, 웹페이지에서 script가 실행되면, 해당 웹 페이지는 실행 중인 script가 종료될 때까지 응답 불가 상태, 즉 다른 일을 처리할 수가 없다.

이때, Web Worker를 사용하면, 시간이 오래 걸리는 javascript 작업 등을 background에서 처리하게 하여, 사용자의 UI를 방해하지 않고, 작업을 수행할 수 있다. 즉, script의 multi thread가 가능하게 된다.


Web Worker를 왜 사용하는가?


Web Worker를 사용하는 이유는, script 작업이 복잡하여 시간이 오래 걸리는 경우에도, 사용자의 다른 UI 작업에 방해를 주지 않기 위한 multi thread가 가능하기 때문이다.

Web Worker의 활용

  • 사용자의 UI 작업(UI thread)에 방해 없이 계속 수행해야하는 작업이 있을 경우
  • background에서 오랜시간 동안 작업해야 하는 경우
  • 원격에 있는 리소스에 대한 액세스 작업(localstorage를 액세스 하는 경우)이 있을 경우
  • 복잡한 수학적 계산 작업이 있을 경우

Web Worker를 사용하기 예전 상황

  • 스크립트가 수행을 하는데 오랜 시간이 걸릴 때, 브라우저는 무반응 스크립트에 대한 경고를 보여줌
  • webworker를 이용한 이후에는 이 문제 해결


Web Worker를 어떻게 사용하는가?


  • How Web Worker works
  • 기본적인 사용방법


How Web Worker Works


How Web Worker Works
출처: How Web Worker Works

Main script가 실행되는 HTML 파일(웹페이지)과 background script가 실행되는 Worker가 있다. HTML 파일(웹페이지)과 Worker간의 데이터를 서로 주고 받으면서, script가 실행되는 방식이다.

이때, 데이터를 송신할 때는 postMessage 메서드를 사용하고, 데이터를 수신할 때는 onmessage 이벤트 핸들러를 사용한다. HTML 파일(웹페이지)에서 데이터를 송신하거나 수신할 수 있고, Worker에서도 데이터를 송신하거나 수신할 수 있다.

postMessage는 다수의 window 창간의 정보교환을 목적으로 사용한다. workers는 DOM에 대한 접근 권한이 없어서 직접 웹페이지를 조작이 불가능하다.


Web Worker의 기본적인 사용방법


코드상으로 Web Worker를 사용하기 위해서는 다음과 같이 해야한다.

  • Worker 실행 파일(worker.js) 작성
  • Worker를 호출할 HTML 파일(웹페이지)에서 Worker를 호출
  • Worker 종료


Worker 실행 파일(worker.js) 작성


Worker가 실행할 script, 즉 javascript(worker.js) 파일을 만들고 작성한다. 이때, on message 이벤트 핸들러와 postMessage 메서드를 같이 작성한다. 왜냐하면, HTML 페이지로부터 전달 받을 데이터를 onmessage 이벤트 핸들러로 수신받아 처리한 후, postMessage 메서드로 다시 HTML 페이지에 전달해줘야 하기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
// worker.js

//웹페이지로부터 메세지를 수신하여 시간이 오래 걸리는 작업 등을 처리
onmessage = function(e){
var receiveData = e.data;

//워커를 호출한 곳(웹페이지)으로 결과 메시지를 전송
if(receiveData) {
var sendData = "I'm working for you"
postMessage(sendData)
}
}


Worker를 호출할 HTML 파일(웹페이지)에서 Worker를 호출


New 연산자와 Worker 생성자 함수를 이용해 worker 인스턴스를 생성한다. 생성한 worker 인스턴스를 사용해서, Worker(worker.js)에 보낼 데이터를 postMessage 메서드를 이용해 작성하고, 후에, Worker(worker.js)에서 처리된 후 받을 데이터를 on message 이벤트 핸들러를 이용해 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
var worker;
function callWorker(){
if(!!window.Worker){ //브라우저가 웹 워커를 지원하는지 확인

if(worker) worker.terminate(); //워커가 이미 존재하면 종료시킴
worker = new Worker("worker.js"); //새로운 워커(객체)를 생성

//워커로부터 전달되는 메시지를 받음
worker.onmessage = function(e){
alert(e.data);
};

//워커에게 메시지를 전달
worker.postMessage("What does Worker do?");
}
else{
alert("The current browser does not support web worker api")
}
}

function stopWorker(){
if(worker){
worker.terminate();
alert("web worker has terminated");
}
}
</script>
</head>
<body>
<button onclick="callWorker()">Call web worker</button>
<button onclick="stopWorker()">Stop web worker</button>
<br>
<input type="text">
</body>
</html>


Worker 종료


Worker 인스턴스는 생성되고 종료될 때까지 계속해서 데이터를 받을 준비를 한다. 그래서, 브라우저나 컴퓨터의 자원을 돌려주기 위해 terminate() 메서드를 사용하여, worker를 반드시 종료해야 한다.

1
worker.terminate();

Ethereum Wallet 및 Keystore 파일 생성 및 암호화 원리

지갑이 생성되는 과정, 그리고 Keystore 파일


지갑을 생성하게 되면, 지갑의 비밀번호(pw), private key(pk), 그리고 지갑을 백업할 수 있는 keystore 파일을 받게된다.

지갑을 import 하는 등 지갑에 대한 본인 인증을 할 때는 2가지 방법 중 하나로 한다.

  • pk를 이용하여 지갑에 대한 본인 인증을 한다.

    • 문제는 pk(64 hex 문자열)를 기억하기 쉽지 않고, 관리 이슈도 있다.
  • Keystore 파일과 pw를 이용하여 지갑에 대한 본인 인증을 한다.

    • Keystore 파일은 pk의 암호화된 버전이다.
    • pk를 pw와 암호화 알고리즘을 이용해 암호화하여 keystore 파일로 만드는 것이다.

이 방법이 더 좋은 2가지 이유가 있다.

  • 안전성:
    • 만약 사용자가 pk만 사용할 경우, 해커는 그 pk만 알아내면 사용자의 지갑은 해킹당하게 된다.
    • 그런데, keystore 파일을 사용하면 해커는 추가로 pw까지 알아야 하므로 더 안전하다.
  • 사용성:
    • 복잡한 pk 대신 사용자들에게 익숙한 pw와 파일로 쉽게 지갑을 사용할 수 있다.


keystore 파일 데이터 구조


keystore 파일의 데이터 구조는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"version": 3,
"id": "4c07993f-ded2-405a-b83d-3b627eebe5cd",
"address": "e449efddf8c9b174bbd40a0e0e1902d6eee72068",
"Crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "7d416faf14c88bb124486f6cd851fa88"
},
"ciphertext":"e99f6d0e37f33124ee3020fad01363d9d7500efce
913aede8a8119229b7a5f2e",
"kdf": "scrypt",
"kdfparams": {
"dklen": 32,
"salt": "c47f395c9031233453168f01b5a9999a06ec97c829
a395ecd16e1ad37102ec7f",
"n": 8192,
"r": 8,
"p": 1
},
"mac": "82078437ee94331c69125eef4001ff4b78b481e909a6
2a9ac25aa916237b70be"
}
}

위 구조 중, Crypto 객체가 keystore 파일 암호화에 대한 정보이다.


pw를 이용해 pk를 암호화/복호화하여 keystore 파일을 생성하는 원리


pw 암호화


pw 암호화 과정
출처: pw 암호화 과정

Pk를 암호화하기 위해 pw를 사용하는데, 이 pw를 직접 암호화키로 사용하지 않고, 이 pw를 암호화하여 암호화한 값을 암호화 키로 사용해서 pk를 암호화한다. 이때, pw를 암호화하는 알고리즘은 Scrypt인데, 비밀번호는 특성상 복호화할 필요가 없기 때문에 단반향 알고리즘 중 하나인 Scrypt를 사용한다.

Keystore 파일에 들어갈 때는, kdf: ‘Scrypt’ 이런식으로 들어가는데, kdf는 암호화 알고리즘 이름을 말하고, Scrypt 알고리즘을 사용한다는 뜻이다.

참고로, kdfparams 들어가는 것 중, n은 CPU/memory 비용을 말한다. 즉, 값이 클수록 암호화 파워가 증가한다.


pk 암호화


pk 암호화 과정
출처: pk 암호화 과정

Pk를 암호화하기 위해서는 좀 전에 pw를 암호화한 값을 암호화키로 사용하고, AES 알고리즘을 사용해서 pk를 암호화한다. pk는 복호화할 필요가 있으므로(pk는 거래를 할 때, 사용되므로) 양방향 알고리즘인 AES를 사용한다.

Keystore 파일에 들어갈 때는, cipher: ‘aes-128-ctr’ 이런식으로 들어가는데, cipher는 암호화 알고리즘의 이름이고 aes-128-ctr 알고리즘을 사용한다는 뜻이다.


Mac


Mac은 keystone 파일을 사용할 때, 사용자가 입력한 pw의 일치여부를 확인하고, pk를 복호화해도 되는지 확인 용도로 사용한다.

*참고로, 이더리움 wallet 만들어지는 순서

  • Pk로 public key 생성
  • Public key로 address 생성

Lifecycle API

Lifecycle API란 무엇인가?


Lifecycle API란, 컴포넌트가 브라우저에서 생성될 때/업데이트 될 때/제거될 때, 호출되는 API이다.

React 컴포넌트의 lifecycle은 다음과 같다.

React 컴포넌트의 lifecycle
출처: React 컴포넌트의 lifecycle


React lifecycle API 3단계


Mounting: React 컴포넌트의 인스턴스가 생성되고, DOM에 삽입되는 과정의 lifecycle
(Mount: 컴포넌트가 DOM에 삽입되는 것)

  • constructor
  • componentWillMount
  • render
  • componentDidMount

Updating: prop 또는 state의 변경으로 해당 컴포넌트가 re-rendering 되는 경우의 lifecycle

  • componentWillReceiveProps(props가 변경될 때만)
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

Unmounting: 컴포넌트가 제거되는 과정의 lifecycle
(Unmount: 페이지 이동으로 인한 그 전 페이지에서의 컴포넌트는 DOM에서 분리되는 것)

  • componentWillUnmount


React v16.3 이전의 Lifecycle API


React v16.3 이전의 Lifecycle API
출처: React v16.3 이전의 Lifecycle API


Mounting


React 컴포넌트의 인스턴스가 생성되고, DOM에 삽입되는 과정의 lifecycle


constructor


우선, lifecycle API 전에 컴포넌트가 새로 만들어질 때마다 제일 먼저 호출되는 것이 바로, constructor(컴포넌트 생성자 함수)이다.

참고로, 컴포넌트에서 state를 정의할 때는 2가지 방법 중 하나를 사용하면 된다.

  • Class fields 문법
  • Constructor 사용

Class fields 문법


1
2
3
4
5
class InvestContainer extends Component {
state = {
number: 0
}
}


Constructor 사용


1
2
3
4
5
6
7
8
class InvestContainer extends Component {
constructor(props) {
super(props);
this.state = {
number: 0
}
}
}


super(props)를 호출한 이유:

ES6 class의 constructor에서 super 메소드를 사용하는 이유는, 자식 클래스에서 super 메소드를 사용함으로써 자식 class의 constructor 내부에서 부모 클래스의 constructor를 호출한다. 즉, 부모 클래스의 인스턴스를 생성해서, this를 사용할 수 있게 된다. 만약 super 키워드를 사용하지 않으면, this에 대한 참조 에러가 발생한다. 다시 말하면, super 메소드를 호출하기 이전에는 this를 참조할 수 없다는 것을 의미한다.

리액트에서 컴포넌트를 class형으로 만들때는, 모두 기본적으로 리액트에 있는 ‘Component’라는 부모 클래스로부터 상속받아 자식 클래스로 시작하게 된다. 그래서 자식 클래스에서는 super 메소드를 사용해서 , 부모 클래스(Component)의 constructor를 호출, 즉 Component 클래스의 인스턴스를 생성해서 this를 사용할 수 있게 되는 것이다. 그 다음에, this를 이용해 프로퍼티를 초기화할 수 있다.

정리하면, constructor를 사용한다면, 반드시 super()를 불러와야 한다. super()를 사용하지 않으면, this가 초기화되지 않는다.



그러면, super()에 props를 사용해야 하는가?

constructor 안에서 this.props에 접근하고 싶다면, super(props)를 사용한다.
만약, super() 이렇게만 사용하면 다음과 같이 나온다.

1
2
3
4
5
6
class MyApp extends React.Component{
constructor(props){
super();
console.log(this.props); // this.props is undefined
}
}

위 코드에서 super(props)라고 사용해야 this.props에 접근할 수 있다.

그러나, constructor가 아닌 다른 곳에서 this.props를 사용하고 싶으면, constructor 안에 props를 넘겨줄 필요가 없다. 왜냐하면, React에서 자동으로 세팅해주기 때문이다.

이 방법이 더 복잡하므로, class fields 문법으로 주로 사용하는 것이 더 편리!


componentWillMount


컴포넌트가 DOM에 삽입되기 전(Mount 되기 전)에 호출되는 API이다. 즉, render가 되기 전이기 때문에, 컴포넌트가 브라우저에 나타나기 전(화면에 나가기 전)에 호출되는 API이다.

사용 이유

  • mount 직전에 하고 싶은 것들(i.e. 방문자가 어떤 페이지를 방문했는지 Google Anlaytics에 신호할 때)이 있을 경우 사용
  • 루트 컴포넌트에서 APP과 관련된 외부 API를 설정할 때 사용

React v16.3 이후부터는 해당 API가 deprecated 되어서, 사용하면 안된다.
기존에 이 API에서 하던 것들을 constructor와 componentDidMount에서 처리 가능하다.


componentDidMount


컴포넌트가 DOM과 삽입(Mount)되고, render가 된 이후에 호출되는 API이다.

이 API는 페이지가 첫 렌더링 될 때, 호출되는 API이다. 즉, 이 컴포넌트가 사용된 페이지가 처음 로드 될때, 또는 refresh로 다시 이 페이지가 불러올 때 등으로 인해 처음으로 렌더링 될 때, 호출되는 API이다.

사용 이유

  • DOM에 대한 접근이 필요한 모든 설정을 수행할 때 사용(DOM의 속성을 읽거나 직접 변경하는 작업 등)
  • 해당 컴포넌트에서 필요로하는 데이터를 요청하기 위해 사용(axios, fetch 등을 통한 ajax 요청)
  • 외부 라이브러리 연동
  • addEventListener 적용

예를 들면, 액션을 발생시켜 서버로부터 데이터를 받아올 수 있다.

1
2
3
4
componentDidMount() {
this.props.getEachProject(this.props.projectId);
this.props.getContractList({ projectId: this.props.projectId });
}

또는, addEventListener를 적용할 수 있다.

1
2
3
componentDidMount() {
window.addEventListener("ICONEX_RELAY_RESPONSE", this.eventHandler, false);
}


Updating


컴포넌트의 업데이트는 props 또는 state의 변화에 따라 결정된다.


componentWillReceiveProps


컴포넌트가 새로운 props를 받게됐을 때 호출되는 API이다. 즉, 새로운 props로 어떠한 작업을 수행하기 전에, 이 새로운 props를 인자로하여 이 API가 호출된다. 새로 받게될 props는 nextProps로 조회 가능하고, this.props는 현재의 props를 말한다.

사용 이유

  • props를 받아서 state를 변경해야 하는 경우 유용하다.

다음과 같이 작성할 수 있다.

1
2
3
4
5
componentWillReceiveProps(nextProps) {
if (this.props.percent !== nextProps.percent) {
this.setUpPercent(nextProps.percent);
}
}

또는,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
componentWillReceiveProps(nextProps) {
if (nextProps.message === "") {
this.setState({
hide: true
});
} else {
this.setState({
hide: false
});
setTimeout(() => {
this.setState({
hide: true
});
this.props.handleToast("");
}, 2500);
}
}

이 API는 초기 렌더링시에는 호출되지 않는다.


shouldComponentUpdate


컴포넌트 업데이트 직전에 호출되는 API이다.

사용 이유

  • 컴포넌트를 최적화하는 작업에 사용
    • 즉, 해당 컴포넌트의 re-rendering를 제어할 때 사용

이 API는 항상 boolean(true 또는 false)을 반환한다. 즉, 해당 컴포넌트를 re-rending할지 말지를 결정하는 API이다.

컴포넌트를 최적화하기 위해서는, 필요없는 re-rending을 막아야 하고, 그러기 위해서는 props 또는 state가 변경되었을 때만, re-rendering 하게끔 이 API안에서 코드를 작성하면 된다.

예를 들어, 다음과 같이 작성하면 된다.

1
2
3
4
shouldComponentUpdate(nextProps, nextState) {
return this.props.engagement !== nextProps.engagement
|| nextState.input !== this.state.input;
}


componentWillUpdate


이 API는 update가 발생하기 전 어떤 작업이 필요한 경우 사용하는 API로써, shouldComponentUpdate에서 true를 반환했을 때만, 호출되는 API이다. shouldComponentUpdate가 이미 사용되고 있는 컴포넌트에서 componentWillReceiveProps를 대신 사용한다. 이 API가 호출되고 난 다음에는 render() 함수가 호출된다.

사용 이유

  • 애니메이션 효과를 초기화할 때 사용
  • 이벤트 리스너를 없앨 때 사용

이 API에서는 this.setState()를 사용하면 무한 루프가 일어나게 되므로 사용하면 안된다.

예를 들어, props가 업데이트 되기 전에 어떤 element를 fade out 애니메이션 효과를 주고 싶은 경우에는 다음과 같이 사용한다.

1
2
3
4
5
6
7
8
componentWillUpdate (nextProps, nextState){
if(!nextState.show){
$(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'1'});
}
else{
$(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'0'});;
}
}


componentDidUpdate


componentWillUpdate 이후, render() 함수가 호출된 뒤에 호출되는 API이다.
이 시점에서는 this.props와 this.state가 새로 변경되었고, 대신에 이전 값들인 prevProps와 prevState를 조회할 수 있다.

사용 이유

  • props 또는 state에 따라서 DOM을 업데이트 할 때 사용
1
2
3
4
5
componentDidUpdate(prevProps, prevState) {
if (this.props.location.query.pagename !== prevProps.location.query.pagename) {
LayerPopup.clear();
}
}


Unmounting



componentWillUnmount


컴포넌트가 DOM에서 삭제된(분리된)(=== unmount) 후 호출되는 API이다.

A 페이지에서 로그인 컴포넌트를 사용하고, B 페이지에서는 장바구니 컴포넌트를 사용한다고 하자. 그러면, A 페이지가 로드가 되면, 로그인 컴포넌트가 마운트가 된다. 그러다가, B 페이지로 이동하게 되면, 로그인 컴포넌트는 언마운트 되고, 장바구니 컴포넌트가 마운트가 된다. 즉, 페이지 이동으로 인한 기존 페이지에서의 컴포넌트가 그 페이지의 DOM에서 분리되는 것을 Unmount 라고 한다.

만약 componentDidMount에서 addEventListner를 사용했는데, 다른 페이지에서는 이 addEventListner를 사용하고 싶지 않다면, componentWillUnmount에서 이 컴포넌트가 Unmount 되기 전에, 이 addEventListner를 제거해야 한다.(removeEventListener)

왜냐하면, addEventListner는 window.addEventListner 이렇게 사용하기 때문에(window 객체의 메소드), 전역으로 다른 페이지에도 영향이 간다. 그래서, 이 페이지에만 사용할 것이라면, 이 페이지의 컴포넌트가 Unmount 될 때, addEventListner를 제거해야 한다.

사용 이유

  • removeEventListener로 이벤트 제거
  • 등록했었던 이벤트 제거
  • 컴포넌트 내부에서 타이머나 비동기 API를 사용하고 있을 때, 이를 제거하기에 유용하다.
  • setTimeout 함수를 사용했다면, clearTimeout을 통하여 제거

예를 들어, removeEventListener로 이벤트 제거 할 수 있다.

1
2
3
componentWillUnmount() {
window.removeEventListener("ICONEX_RELAY_RESPONSE", this.eventHandler, false);
}


React v16.3 이후의 Lifecycle API


React v17 부터 depreciated 될 API

  • componentWillMount
  • componentWillReceiveProps(v17: getDerivedStateFromProps)
  • componentWillUpdate(v17: getSnapshotBeforeUpdate)

React v17 부터 추가될 API

  • componentDIdCatch: 컴포넌트 에러 핸들링 API

React v16.3 이후의 Lifecycle API
출처: React v16.3 이후의 Lifecycle API


React v16.3 이후에 변경된 부분


몇개 lifecycle API가 변경된 부분이 있다. 아래의 링크에서 확인해보자.

출처: React v16.3 이후의 lifecycle