Front-End 개발환경 구축

이제 프론트엔드Front-End를 다루겠습니다.

Frond-end를 위한 웹페이지 개발은 사실 저도 전문가가 아닙니다. 따라서 자세한 설명은 어렵습니다. 그래도 최대한 이해하고 설명해 보겠습니다. 이점은 양해 바랍니다.


개발환경

  • OS: Ubuntu 16.04 64bits
  • 브라우저: chrome
  • Editor: atom


패키지 설치

웹페이지 개발을 위해 다음과 같은 것들을 사용할 예정입니다.

  • 로컬 웹서버 구축
  • HTML5
  • Node.js
  • Javascript
  • React
  • Next.js


다음과 같이 몇 가지 패키지를 설치합니다. 특히 Next.js의 경우는 다음과 같이 특정 버전을 지정하여 설치합니다.

$ npm install --save next@4.1.4 react react-dom


Next를 설치하고 나면 .next라는 폴더가 생성됩니다.


Next가 페이지 라우팅, 즉 페이지들간의 이동을 담당하게 됩니다. 이때 페이지가 저장된 폴더명을 반드시 pages로 해야 합니다. 저는 이미 사전에 프로젝트 폴더 밑에 pages 폴더를 만들어 놨습니다. Next의 유용한 점은 pages 밑의 파일이 하나의 웹페이지가 되어 자유롭게 이동하게끔 해주는 것입니다.


로컬 웹서버 구축

로컬 웹서버 구축은 별로 할 것이 없습니다. Next만 설치하면 끝입니다!
Next를 사용하기 위해 package.json 파일에 다음과 같이 "dev"라는 스크립트 관련 내용을 추가합니다. dev라는 스크립트를 실행할 때마다 "next dev"가 실행됩니다.
image.png


그럼 바로 실행해 보겠습니다. 프로젝트의 root 폴더로 이동하여 다음과 같이 입력합니다.

$ npm run dev

image.png


그러면 다음과 같이 로컬 웹서버가 돌아간다는 메시지가 나타납니다. 웹브라우저를 켜고 http://localhost:3000을 입력합니다.

그러나 404 Not Found가 나타납니다. 그것은 pages 폴더에 아무 것도 없기 때문입니다. 테스트용으로 pages 폴더 밑에 new_story.js라는 파일 이름으로 다음과 같이 내용을 넣어봅니다.

import React from 'react';
export default () => {
  return <h1>This is the new dream story page!</h1>;
};


단순히 react 패키지를 import 하고, 메시지를 출력하는 페이지입니다. 내용을 저장하고 이번에는 웹브라우저에 http://localhost:3000/new_story.js를 입력하면 다음과 같이 웹페이지가 나타납니다. 이때 new_story.js에서 .js는 빼고 입력해야 합니다. 주의하세요.
image.png


비록 내용은 아직 이해하기 어렵지만 너무 간단한거 아닌가요? 저도 사실 놀랍습니다. 이렇게 웹페이지 구축이 쉽다니요.


index.js

웹사이트를 방문하면 처음에 보이는 페이지가 있습니다. 이런 페이지는 특별히 페이지 이름을 지정하지 않아도 되는데, 이 페이지를 index 페이지라고 합니다. pages 폴더에 index.js라는 파일을 다음과 같은 내용으로 만듭니다.

import React from 'react';
export default () => {
  return <h1>This is the DreamFactory page!</h1>;
};


그리고 다시 웹브라우저에서 'http://localhost:3000`으로 페이지 이름을 넣지 않아봅니다.
image.png

짜잔~ 멋지지 않습니까?



개발환경을 구축하면서 index 페이지를 꾸며볼 것입니다. 웹페이지 구성은 앞서 간단히 소개했었습니다.

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


dream_factory.js

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

// import the web3.js
import web3 from './web3';
// import DreamFactory contract
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)가 선택되어 있어야 합니다. 다음 그림처럼요. 즉  Metamask 로그인 및 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 다큐먼트를 만들어 출력해야 하니까요. 모바일 기기로 접속하는 경우 매우 중요한 부분입니다. Server-side Rendering의 의미는 서버에서 사용자 웹브라우저에 표시될 내용을 생성한다는 것입니다. 이 때, 서버에서 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/your_api_key'
  );
  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은 과연 서버 중심의 웹의 많은 부분을 대체할 수 있을까요?



 [한빛미디어 블록체인 도서 보러가기]