View Download 페이지 구현

이제 마지막 페이지를 꾸며볼 차례입니다. 원래 구상은 이런 페이지입니다.

딱 느낌으론 별거 없어 보입니다. 단순히 Downloads 배열을 읽어서 테이블에 넣어 주면 되겠죠. 한 가지 난관이 있습니다. 앞서 잠시 소개했던 바로 이것!

솔리티디에서는 구조체 배열을 리턴할 수 없다!

이 산만 넘으면 수월해 보입니다.


테이블 만들기

먼저 쉬운 것부터 해보죠. 다운로드 기록을 표시하기 위한 테이블을 만듭니다. 테이블 또한 semantic-ui-react를 이용해서 간결하게 만듭니다. 내용을 보면 어렵지 않게 이해를 할 수 있습니다. 테이블과 더불어 "Request Download" 버튼을 만들어서 다운로드 페이지로 이동할 수 있게 했습니다.

render() {
    // get required tags at once from Table
    const { Header, Row, HeaderCell, Body }= Table;
    return (
      <Layout>
        <h2>Downloads List</h2>
        <Link route={`/dream_stories/${this.props.address}/request_download`}>
          <a>
            <Button primary floated="right" style={{ marginBottom: 10 }}>
              Request Download
            </Button>
          </a>
        </Link>
        <Table>
          <Header>
            <Row>
              <HeaderCell>ID</HeaderCell>
              <HeaderCell>Downloader</HeaderCell>
              <HeaderCell>Download Price</HeaderCell>
              <HeaderCell>Date</HeaderCell>
            </Row>
          </Header>
          <Body>
            {this.renderDownloadRows()}
          </Body>
        </Table>
        <p>{this.props.downloads_count} Downloads</p>
      </Layout>
    );
  }

중간에 {this.renderDownloadRows()} 부분에 함수가 하나 사용됐습니다. 이 함수는 조금 이따 알아볼텐데요, 다운로드 배열을 읽어서 테이블을 채우는 함수입니다.


Downloads 배열 읽기

이제 어려운 산을 넘을 차례입니다. 그렇게 어렵지 않습니다. React 코드가 조금 복잡해 보일 수 있으나 코드를 이해하면 별거 아니라고 느낄 것입니다.

// class based component
class DownloadsList extends Component {
  static async getInitialProps( props ) {
    // get the contract address from url
    // this expression is to just get the address from props.query object
    const { address }= props.query;
    // get DreamStory instance
    const story= dream_story( address );
    // get the number of downloads`
    const downloads_count= await story.methods.getDownloadsCount().call();
    // get the download struct instance one by one
    const downloads= await Promise.all(
      // Generate array of indices and fill them
      Array( parseInt(downloads_count) ).fill().map( (element, index) => {
        return story.methods.downloads(index).call();
      })
    );
    return { address, downloads, downloads_count };
  }

  • getInitialProps에서 필요한 값을 읽어옴
  • URL로 부터 address를 추출하여 DreamStory 컨트랙트 인스턴스 생성
  • downloads의 배열의 크기를 읽어옴
  • Array( parseInt(downloads_count) ).fill()의 역할은 단순히 downloads_count 만큼 index 배열을 만들어 줌. 예) downloads_count가 5라면 결과값은 {0, 1, 2, 3, 4, 5}. 즉 이 결과를 이용하면 downloads 배열의 요소 하나씩 접근할 수 있음
  • map함수를 이용하여 downloads 배열 요소 하나씩 접근하여 download 객체 값을 얻어옴

앞서 설명했듯이 for 문이나 이런걸 쓰지 않고 downloads의 모든 값을 하나씩 읽어 오게 구현했습니다. 이렇게 하면 각 download를 병렬로 읽어올 수 있습니다. for 문처럼 차례로 하나씩 읽는 방식보다 효율적입니다.


테이블 Cell 채우기

이제 테이블을 채울 차례입니다. 테이블의 행을 하나씩 하나씩 채우는게 아니라 배열에서 값을 하나씩 읽어와서 채우는 효율적인 방식을 택합니다. 이를 위해 별도의 파일을 만듭니다. 파일 이름은 download_row.js라고 하고 components 밑에 생성합니다.

import React, { Component } from 'react';
// import Table`
import { Table } from 'semantic-ui-react';
// import web3
import web3 from '../ethereum/web3';

const Timestamp = require('react-timestamp');

// class based component
class DownloadRow extends Component {
  //
  render() {
    // get required tags
    const { Row, Cell }= Table;
    // shorten the properties
    const { id, download }= this.props;
    return (
      <Row>
        <Cell>{ this.props.id }</Cell>
        <Cell>{ download.downloader }</Cell>
        <Cell>{ web3.utils.fromWei( download.price_wei, 'ether' ) }</Cell>
        <Cell><Timestamp time={download.date} format='full'/></Cell>
      </Row>
    );
  }
}

// export the component
export default DownloadRow


이 파일의 용도는 테이블의 행 하나를 반복적으로 생성하여 테이블의 셀을 채우는 것입니다. 즉, Download 구조체가 전달되면 구조체에서 각각의 요소값을 읽어서 지정된 셀에 값을 대입하는 것입니다.
실제 구조체 값을 전달하는 부분은 아직 표시하지 않았습니다. 위 코드의 중요 내용은 this.props에서 값을 읽어서 행 하나를 채우는 것입니다. <Timestamp> 태그는 조금 이따가 설명하겠습니다.

그럼, 이제 download_row에 값을 전달하는 부분을 알아보겠습니다. downloads_list 페이지에 다음과 같이 구현해 줍니다. 좀 전에 얘기했던 renderDownloadRows() 함수입니다.

(생략)
  // render all download rows
  renderDownloadRows() {
    return this.props.downloads.map( (download, index) => {
      return <DownloadRow
        key={index}
        id={index}
        download={download}
        address={this.props.address}
      />;
    });
  }

  render() {
(생략)

  • getInitialProps에서 읽은 downloads 배열에 하나씩 접근
  • <DownloadRow> 컴포넌트에 전달할 값을 세팅
  • {key}는 React에서 컴포넌트 리스트를 표시하기 위해서 필요함
  • {id}, {download}, {address}를 DownloadRow 컴포넌트에 전달
  • 전달된 내용은 this.props.xxxx 으로 접근 가능

테이블에 값을 넣기 위한 모든 과정이 끝났습니다. 다시 요약해보면, 먼저 downloads의 배열을 병렬적로, 효율적으로 읽은 후 download 하나씩 DownloadRow에 전달하여 테이블의 행을 만듭니다.

마지막으로, timestamp 표시에 대해서 잠시 언급하겠습니다.


timestamp를 날짜로 변경

원래 컨트랙트 소스 코드에서는 download 구조체에 date라는 변수는 uint 타입으로 timestamp를 저장하고 있습니다. 이 값의 정의는 이렇습니다.

current block timestamp as seconds since unix epoch
유닉스의 초기 가동일 기준으로 현재의 날짜를 초로 표시한 숫자입니다.

예를 들어 1450663457이런 긴 숫자입니다. 이걸 사람이 알아 볼 수 있는 형태로 만들어 줘야 합니다. 직접 코드를 만들 수도 있겠죠. 유닉스 시작일로 부터 계산된 초를 가지고 며칠, 몇 시간이 지났는지 계산하면 됩니다. 번거로우니 그냥 패키지를 사용하겠습니다.

다음과 같이 react-timestamp를 설치합니다. 사용법은 https://www.npmjs.com/package/react-timestamp를 참고하세요.

$ npm install --save react-timestamp


다양한 형태로 날짜를 표현할 수 있습니다. 여기서는 <Cell><Timestamp time={download.date} format='full'/></Cell>로 사용했습니다. 그 결과는요?

위 코드를 모두 반영한 결과, 다음과 같이 멋있는 테이블이 만들어 졌습니다.
image.png



구현하고자 했던 것은 거의 다 구현한거 같습니다. 한 가지 빠진 것이 author가 컨트랙트에 쌓인 잔액을 인출하기 위해 contributors의 동의를 얻어서 처리하는 부분입니다. 이것은 어떻게 처리하는게 좋을지 고민 좀 해보고 구현하도록 하겠습니다.

또, 구현하면서 새롭게 넣은 내용이 Story 검색 기능입니다. 검색바만 만들어 놓고 구현은 안했죠. 이것도 방법을 좀 찾아서 구현할 예정입니다.

또... Download 저작권을 확인할 수 있는 방법...

또, Dream Story에서 Dream Signs을 찾아주는 서비스... 또 유사한 Dream Story를 매칭하는 서비스...

끝도 없네요. 그래도 초기 계획했던 것들을 많은 부분 달성해서 뿌뜻하고 좋습니다. 정말 열심히 달려온거 같습니다.



오늘의 실습: 스토리를 올려 모은 잔액을 author가 인출하는데 contributors의 동의를 받을 필요가 있을까요?