[DreamChain DApp] #16 Front-End 개발환경 구축 2

in #kr6 years ago

이전글에 이어서 개발환경 구축하는 내용입니다.

이전글 - [DreamChain DApp] #15 Front-End 개발환경 구축 1

본 내용은 Ethereum and Solidity: The Complete Developer's Guide을 참고해서 작성되었습니다.


개발환경을 구축하면서 index 페이지를 꾸며볼 것입니다. 웹페이지 구성은 아래글에서 간단히 소개했었습니다.
[DreamChain DApp] #2 Front-End 화면구성 및 Routing

이중에서 index 페이지는 이런 형태입니다.

여기서 단순히 웹페이지만 꾸미면 끝나는게 아니라 배포한 스마트 컨트랙트와 연동하는 작업이 필요합니다. index 페이지를 만들기 위해서는 다음과 같은 작업들이 필요합니다.

  • web3 provider 구성 (provider는 Metamask의 provider 기능 활용)
  • 배포된 컨트랙트 주소를 web3에 연동
  • DreamFactory 인스턴스를 이용하여 배포된 DreamStory 주소들을 획득
  • 각 DreamStory의 내용을 React 컴포넌트를 이용하여 표시

한가지 주의할 것은 글을 쓰면서 index.js, web3.js 파일을 여러 번 수정할 것입니다. 이유는 웹페이지 구성하는 방법을 설명하기 위해서 입니다. 단순히 결과물만 나타내지 않고, 하나 하나 작업하는 내용을 보여드릴 것입니다.

web3 provider 구성

web3 provider 구성을 위해 여기서는 metamask의 기능을 이용합니다. web3 provider는 접속하고자 하는 블락체인 네트워크의 인터페이스라고 생각하면 됩니다. 즉, 특정 네트워크에 연결하려면 특정 provider가 필요합니다. provider를 설정하는 것은 번거로운 일입니다. 이전에 deploy.js에서는 HDWalletProvider와 Infura API를 이용하여 provider를 구성했었습니다. 여기서는 이런 작업을 일단 단순화 하고자 브라우저에서 제공하는 Metamask의 provider를 이용합니다. 브라우저에 Metamask가 깔려 있어야 합니다.
Metamask가 깔려 있다면 아래와 같이 ethereum 폴더 밑에 web3.js라는 이름으로 파일을 만들고 아래와 같이 코드를 입력합니다.

// import web3, Web3 is a constructor function
import Web3 from 'web3';

// create a web3 instance by accessing the provider of metamask
// @note a user should install metamask inside the browser for this
const web3= new Web3( window.web3.currentProvider );

// export the web3 instance
export default web3;

다시 한번 여기서 중요한 점은 DreamFactory 웹서비스를 이용하려고 하는 사용자는 Metamask가 깔여 있어야 합니다. 위 코드를 보면 브라우저내에 존재하는 metamask의 provider를 이용하여 web3 인스턴스를 생성하고 있습니다. 나중에 Metamask를 설치하지 않는 사용자들을 위해 코드를 변경할 것입니다. 조금 불편하겠지만, 일단 web3 구성을 좀 편한 방법으로 하겠습니다.

dream_factory.js

web3 provider가 구성되었기 때문에 이제 이를 이용하여 배포한 DreamFactory 컨트랙트와 인터페이스를 할 수 있습니다. 이번에는 배포된 DreamFactory 컨트랙트가 여러 파일에서 import 하여 쓰일 수 있도록 스크립트를 다음과 같이 만듭니다.

// import the web3.js
import web3 from './web3';
// import interface, which is abi
import DreamFactory from './build/DreamFactory.json';

// create dream factory instance
const factory_instance= new web3.eth.Contract(
  // set contract interface from the pre-built json file
  JSON.parse( DreamFactory.interface ),
  // deployed DreamFactory contract address (change it to yours)
  '0x75A31f56efEba84D7A1D99ac1b29Bb062cCD57d9'
);

// export the factory instance
export default factory_instance;

위 코드에서 DreamFactory 컨트랙트 주소는 여러분의 주소를 입력합니다.

이렇게 만들어 놓으면 DreamFactory 컨트랙트와 인터페이스가 필요한 곳에서 간단히 위 파일을 import하여 사용하면 됩니다.

Dummy DreamStory 배포

이제 index 페이지에 DreamStory를 표시할 준비가 됐습니다. 그런데 지금은 아무 DreamStory도 없는 상태라 표시하고 싶어도 못합니다. 따라서 테스트용으로 표시할 DreamStory 컨트랙트를 테스트넷에 배포합니다. 여기서는 간단히 Remix를 이용합니다. 상세한 내용은 이전글을 참고해 주세요. 여기서는 간략히만 설명합니다.
아래와 같이 배포된 DreamFactory 컨트랙트의 주소를 Remix의 At Address에 입력합니다. 이 때, Environment 탭에 Injected Web3 (Rinkeby)가 선택되어 있어야 합니다. 아래 그림처럼요.

그 다음 createDreamStory탭에 적절한 값을 입력하고 DreamStory를 생성, 배포합니다. 그러면 조금 있다가 해당 트랜잭션이 채굴되어 etherscan 웹사이트에서도 볼 수 있습니다.

Server-side Rendering

index 페이지에 배포된 DreamStory 컨트랙트 주소를 표시하기 전에, 임의로 문자열을 index.js에 출력해는 코드를 만들어 봅니다. 아래 코드는 일반적인 React 방식인데, 결과적으로 동작하지 않습니다만, Next 동작 방식 설명을 위해 추가합니다. 다음과 같이 index.js 파일을 수정해 줍니다.

// import react
import React, { Component } from 'react';
// import DreamFactory instance
import dream_factory from '../ethereum/dream_factory';

// class based component
class FactoryIndex extends Component {
  async componentDidMount() {
    const stories= await dream_factory.methods.getDeployedDreamStories().call();

    console.log( stories );
  }

  // render the component
  render() {
    return <div>Factory Index</div>
  }
}

// export the component so that the next can use this
export default FactoryIndex;

내용이 좀 어려울 수 있습니다만, 주석을 참고해 주세요. 작업하면서 점점 주석의 질도 높아질 것입니다. 특이한 점은 기존에 functional component 방식을 썼는데, 위 코드는 class based component 방식입니다. 그 이유는 웹페이지에 나타날 컴포넌트가 준비가 되면 표시할 데이터를 componentDidMount라는 함수를 사용하여 읽어 들일 수 있기 때문입니다. 즉 컴포넌트가 마운트 되면 표시할 데이터를 불러 들인 후 화면에 표시할 수가 있는 것이죠. 클래스 컴포넌트로 만들면 이처럼 생명 주기(Life Cycle)와 관련된 함수들을 사용할 수 있습니다.

자, 이제 다시 웹브라우저로 가서 새로고침을 해봅니다. 헉 그런데 다음과 같은 에러가 발생합니다. 그러나 놀라지 않으셔도 됩니다. 말씀드렸다시피 에러가 발생하게 되어 있으니까요.
image.png

React와 달리 Next.js는 Server-side Rendering 방식입니다.

Server-side Rendering: 사용자가 Next 서버에 접속하면 Next는 자바스크립트 코드를 Next 서버측에서 실행하여, HTML 다큐먼트를 생성하여 사용자의 브라우저에 전송합니다. 이 방식의 장점은 브라우저에 표시되는 내용이 매우 빠르게 나타나는 것입니다. 왜냐하면 서버에서 실행하여 화면에 표시되어야 하는 HTML 다큐먼트를 보내기 때문입니다. 안그러면 코드를 모두 전송하여 브라우저에서 코드를 실행해서 HTML 다큐먼트를 만들어 출력해야 하니까요. 모바일 기기로 접속하는 경우 매우 중요한 부분입니다. HTML 다큐먼트를 전송한 후, 잠시 후 앱의 자바스크립트 코드를 브라우저에 전송하고, 브라우저에서 다시 한 번 코드가 실행됩니다.

위에 나타난 "window is not defined" 오류의 원인은 다음과 같습니다. 앞에서 작성한 web3.js는 Next 서버에서 한번 실행되고, 자바스크립트 코드가 브라우저에 전송된 후 브라우저에서 다시 한번 실행됩니다. web3.js가 Next 서버에서 실행될 때, 위 문제가 발생하는 것입니다. 왜냐하면 window라는 글로벌 변수는 브라우저에서만 사용할 수 있기 때문입니다. 즉 서버는 브라우저가 아니므로, 사용할 수가 없는 것입니다. 이를 확인하는 방법은 아래와 같이 간단히 node 콘솔에서 확인하는 방법입니다.
image.png

이를 해결하는 방법은 Next 서버에서 Rinkeby 네트워크에 접속해서 필요한 정보를 가져오는 것입니다. 그렇게 되면 사용자의 Metamask 사용 여부와 상관없이 앱이 동작할 수 있습니다.
이를 위해 web3.js 파일을 다음과 같이 수정합니다.

// import web3, Web3 is a constructor function
import Web3 from 'web3';

// declare a variable
let web3;

// handle client side rendering and metamask user
if( typeof window !== 'undefined' && window.web3 !== 'undefined' ){
  // we are in the browser and metamask is running,
  // so use the provider injected by metamask without considering the web3 version
  web3= new Web3( window.web3.currentProvider );
}
// handle server-side rendering and non-metamask user
// the window variable is undefined
else{
  // we are on the server or the user is not running metamask
  // make own provider through infura
  const provider= new Web3.providers.HttpProvider(
    // put your infura api key for rinkeby
    'https://rinkeby.infura.io/v3/6ee87ef7d0d448daba84942b01eeb420'
  );
  web3= new Web3( provider );
}

// export the web3 instance
export default web3;

이렇게 수정 후 브라우저를 새로고침하면 에러가 사라지고, 개발자 콘솔에 배포된 DreamStory 컨트랙트 주소가 나타납니다. 주의할 것은 index 페이지에 표시되는게 아니라 브라우저 콘솔창에 표시됩니다. 참고로 크롬에서 개발자 콘솔을 여는 방법은 단축키로 Ctrl+Shift+C입니다. 콘솔탭으로 이동하면 아래와 같은 화면이 보입니다.
image.png

Remix에서도 확인할 수 있습니다. 이미 배포된 DreamStory 컨트랙트의 주소를 DreamFactory 컨트랙트가 관리하기 때문입니다.
image.png

컨트랙트 정보를 브라우저에 표시하기

그럼 콘솔에 표시된 컨트랙트 주소를 index 페이지에 표시하는 방법에 대해서 알아보겠습니다.
index.js에 아래와 같이 componentDidMount라는 전통적인 React 코드를 사용했었습니다.

class FactoryIndex extends Component {
  async componentDidMount() {
    const stories= await dream_factory.methods.getDeployedDreamStories().call();

그러나 Next에서는 componentDidMount를 사용할 수가 없습니다. 이유는 Next가 server-side 렌더링 방식이기 때문입니다. componentDidMount함수는 브라우저에 컴포넌트가 마운트 된 이후에 호출되는 함수이기 때문에 서버에서는 호출되지 않는 함수입니다. 따라서 초기값을 별도로 설정하는 함수 getInitialProps()를 사용해야 합니다.

getInitialProps함수를 사용하도록 index.js를 다음과 같이 수정합니다.

// import react
import React, { Component } from 'react';
// import DreamFactory instance
import dream_factory from '../ethereum/dream_factory';

// class based component
class FactoryIndex extends Component {
  // get initial properties
  // use static to accelerate the rendering.
  // static functions belong not to instance but to class
  static async getInitialProps() {
    const stories= await dream_factory.methods.getDeployedDreamStories().call();
    // return stories object
    // if we use ES6 code the following can be condensed to return { stories }
    return { stories : stories };
  }

  // render the component
  render() {
    // now whenever the component is rendered, the getInitialProps() is called before.
    // so we can access the stories object
    return <div>{this.props.stories[0]}</div>;
  }
}

// export the component so that the next can use this
export default FactoryIndex;

위 코드에서 getInitialProps()에서 획득한 DreamStory 컨트랙트 주소들을 return <div>{this.props.stories[0]}</div>로 웹페이지에 표시하도록 되어 있습니다. 이렇게 한 후 웹페이지를 새로고침하면 드디어 짜잔하고 컨트랙트 주소가 웹페이지에 나타나게 됩니다.
image.png

Server-side 렌더링 테스트

서버 사이드 렌더링이 정말 맞는지 확인하는 테스트를 해보겠습니다. 서버 사이드 렌더링은 서버에서 component가 실행되어 HTML document로 브라우저에 전달되고, 그 이후에 component 자바스크립트 코드가 전달된다고 했습니다. 즉 component가 서버에서 실행되어 브라우저에 전달되어 먼저 화면에 보여진 후, 그 다음에 다시 브라우저에서 자바스립트 코드를 실행하여 보여줍니다. 그럼, 브라우저에서 자바스립트 실행을 금지하면 어떻게 될까요? 아래와 같이 크롬 브라우저에서 자바스크립트 실행을 'block'할 수 있습니다. 브라우저에서 자바스크립트 실행을 막았지만 서버에서 실행하여 브라우저에 전달되었기 때문에 브라우저에 문제 없이 표시가 됩니다.
image.png

보시다시피 서버 사이드 렌더링의 경우, 사용자에게 빠르게 결과를 보여줄 수 있습니다. 특히 사용자가 모바일 장치를 사용하고, 인터넷이 느리다면 더욱 더요. 일단 서버에서 실행하여 화면에 표시할 내용만 빠르게 전달하고, component 자바 스크립트 코드는 그 이후에 천천히 전송되도록 하니까요.

테스트를 끝냈으니까 브라우저에서 자바스크립트를 다시 허용으로 바꿔줍니다. 대세는 서버 사이드 렌더링이라고 하니, 이 방식을 따라줘야 겠네요~


오늘도 힘차게 달려왔습니다. 저도 글을 작성하며 많은 것들을 배웁니다. 웹의 세계는 정말 방대한 거 같습니다. 변화도 매우 빠르고요. 웹이 세상을 지배하는 시대지만, 곧 블락체인 기술이 과거 앱처럼 번져 나갈것이라고 봅니다.

드디어 다음편부터 본격적으로 웹페이지 꾸미기에 들어갑니다! 응원 부탁드려요~


오늘의 실습: 블락체인 DApp은 과연 서버 중심의 웹의 많은 부분을 대체할 수 있을까요?

Sort:  

(jjangjjangman 태그 사용시 댓글을 남깁니다.)
[제 0회 짱짱맨배 42일장]5주차 보상글추천, 1,2,3,4주차 보상지급을 발표합니다.(계속 리스팅 할 예정)
https://steemit.com/kr/@virus707/0-42-5-1-2-3-4

5주차에 도전하세요

그리고 즐거운 스티밋하세요!

Coin Marketplace

STEEM 0.18
TRX 0.14
JST 0.030
BTC 58559.96
ETH 3156.41
USDT 1.00
SBD 2.44