index 페이지 꾸미기

그럼 이번에는 블록체인과 연동하여 index 페이지를 그럴싸하게 꾸며보겠습니다.


Semantic UI React

웹페이지를 CSS로 꾸미는 것은 매우 번거로운 일입니다. 그래서 번거로운 작업을 덜어 주는 툴킷이 있습니다. 바로 Semantic UI React입니다. 먼저 다음과 같이 패키지를 설치합니다.

$ npm install --save semantic-ui-react


설치가 완료되면 https://react.semantic-ui.com/로 접속해 봅니다. 그러면 온갖 웹 컴포넌트의 코드를 얻을 수 있습니다.
image.png


버튼을 하나 살펴보겠습니다. 다음과 같이 소스 코드를 복사하여 사용자 입맛에 맞게 변경해서 사용하면 됩니다. 앞서 봐왔던 React로 구성되어 있습니다. 사실 저도 React, Next는 전혀 모르고 있었는데 이 프로젝트를 진행하며 알게 되었습니다. 따라하면 되니 좋습니다!

import React from 'react'
import { Button } from 'semantic-ui-react'

const ButtonExampleEmphasis = () => (
  <div>
    <Button primary>Primary</Button>
    <Button secondary>Secondary</Button>
  </div>
)

export default ButtonExampleEmphasis


그런데 위 코드를 보면 스타일링을 담당하는 CSS에 관한 내용은 없습니다.
image.png


다시 semantic-ui 웹사이트 내용을 참고합니다. 두 가지 방법이 있는데 Next.js는 기본적으로 css 패키지 설치 방식을 지원하지 않습니다. 방법은 있지만 단순하지 않아서 여기서는 stylesheet link를 사용하는 방식을 택합니다. 이 다음index.js 파일의 태그에 다음 코드를 삽입하여 스타일링을 합니다.

 <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css"/>

그러면 다음과 같이 semantic-ui 중에서 Card.group를 사용하기 위해 index.js 파일을 수정합니다. 사실 위 스타일링 링크를 return에 직접 삽입하는 방식은 임시방편입니다. 이것도 나중에 다시 수정하도록 할 것입니다. 일단은 빨리 예쁜 페이지를 보기 위한 것이라고 생각해 주세요. 아래와 같이 코딩하면 DreamStory 하나하나가 웹페이지에 Card 형태로 표시됩니다.

// import react
import React, { Component } from 'react';
// import Card UI component only
import { Card } from 'semantic-ui-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 card groups to display stories
  renderStories() {
    // map calls the arugment function one time for every element inside stories array
    // fluid option is to extend the component
    const items= this.props.stories.map( address => {
      return {
        header: address,
        description: <a>View Story</a>,
        fluid: true
      };
    });

    return <Card.Group items= {items} />;
  }

  // render the conponent
  render() {
    // now whenever the component is rendered, the getINitialProps() is called before.
    // so we can access the stories objects
    return (
      <div>
        <link
          rel="stylesheet"
          href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css"
        />
        { this.renderStories() } 
      </div>
   );
  }
}

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


이렇게 하고, 웹페이지를 새로고침하면 다음과 같이 CSS가 입혀진 화면이 표시됩니다. Card처럼 보이지 않는 이유는 카드가 페이지의 width를 모두 차지하기 때문입니다. 적절한 margin을 주면 예쁘게 표시될 것입니다. 그리고 "View Story Details"가 파랗게 링크로 표시된 것을 알 수 있습니다.
image.png


이번에는 예쁜 버튼을 하나 추가해 보겠습니다. 사용할 버튼은 "Labeled Icon" 버튼입니다. https://react.semantic-ui.com/에 접속하면 다음과 같은 코드를 볼 수 있습니다.
image.png


버튼을 추가하기 위해 index.js 파일에 아래와 같이 태크 내용을 추가합니다.

(생략)
<Button
  floated="right"
  content="Create a Story"
  icon="add"
  primary={true}
/>
{ this.renderStories() }
(생략)

그리고 나서 웹페이지를 새로고침하면(npm run dev를 새로 실행할 필요가 없습니다!) 다음과 같이 파란색 예쁜 버튼이 아이콘과 함께 나타납니다. 아이콘은 위 코드의 icon="add"코드로 인해서 나타나는 것입니다. 미리 지정된 더하기(add) 모양의 아이콘을 지정한 것입니다.

image.png


Layout 만들기

많은 웹페이지를 보면 페이지가 바뀌어도 변하지 않는 부분이 있습니다. 대표적으로 페이지 상단에 표시되는 Header라는 부분과 페이지 하단에 표시되는 Footer라는 것입니다. 지금 만들고 있는 웹페이지 또한 Header와 Footer를 갖도록 할 것입니다. 이런 경우 웹페이지마다 매번 동일한 내용의 코드를 넣어야 하는데, 뭔가 비효율적입니다. 그래서 Layout을 만들 것입니다. 즉, Header와 Footer의 코드를 만들어 동일한 내용을 표시하고, 그 안에 내용물(contents)만 웹페이지별로 다른 내용을 채워 넣는 것입니다.

그러나 한 가지 문제가 있습니다. Next는 페이지마다 공통된 부분을 삽입하는 것이 간단하지는 않습니다. 그래서 여기서는 별도의 폴더에 layout.js 파일을 만들어서 각 페이지마다 표시하고 싶은 내용이 layout 안에 표시되도록 하는 방식을 택합니다. 다음 그림과 같이 components 폴더 밑에 layout.js라는 파일을 만듭니다.
image.png


그런 후 layout.js 파일에 다음과 같이 입력합니다.

import React from 'react';

export default ( props ) => {
  return (
    <div>
      <h1>Header</h1>
      {props.children}
      <h1>Footer</h1>
    </div>
  );
};

props.children이란 부분을 주의깊게 보세요. functional component에서 props이란 인자를 받아서 그 children을 Header와 Footer 사이에 표시하고 있습니다. 즉 표시하고자 하는 컴포넌트(props.children)를 layout.js의 태그로 감싸면 그 내용이 Header와 Footer 사이에 표시되게 됩니다.

지금 Header와 Footer는 단순히 텍스트이지만 곧 내용을 채울 것입니다. 그전에 index.js를 다음과 같이 수정하여 Layout이 제대로 동작하는지 확인합니다. layout.js를 import하고, return 부분에 <Layout> 태크로 컴포넌트를 감싸줍니다.

(생략)
// import layout file
import Layout from '../components/layout';
(생략)
render() {
    return (
      <Layout>
        <div>
          <link
            rel="stylesheet"
            href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css"
          />
          <h3>Dream Stories on Sale</h3>
          { this.renderStories() }
          <Button
            content="Create a Story"
            icon="add"
            primary={true}
          />
        </div>
      </Layout>
    );
  }


이렇게 한 후 브라우저를 새로고침하면 다음과 같이 Header, Footer사이에 contents가 나타납니다.
image.png


Header 꾸미기

그러면 이제 계획한 Header를 꾸며 보겠습니다. 사용할 컴포넌트는 Menu라는 것입니다. 다음과 같이 Menu 컴포넌트를 semantic-ui-react에서 import합니다. 그리고, Header의 내용을 Menu를 이용하여 꾸며줍니다.

import React from 'react';
import { Menu, Input } from 'semantic-ui-react';

export default () => {
  return (
    <Menu style={{ marginTop: '10px' }}>
      <Menu.Item>
        DreamChain
      </Menu.Item>
      <Menu.Menu position="right">
        <Menu.Item>
          <Input icon='search' placeholder='Search...' />
        </Menu.Item>
        <Menu.Item>
          Dream Stories
        </Menu.Item>
        <Menu.Item>
          +
        </Menu.Item>
      </Menu.Menu>
    </Menu>
  );
};

Header에 검색 기능을 넣고 싶어서 원래 디자인에서 약간 변경해 봤습니다. 그리고 이 코드에서는 top margin이 설정되게 했습니다. 그리고 Create a Story버튼을 오른쪽으로 이동시키기 위해 position 옵션에 right를 입력합니다.

한 가지 더 변경이 필요한데, 바로 index.js에서 버튼의 렌더 순서와 DreamStory 주소 렌더 순서를 다음과 같이 변경합니다.

(생략)
      <Layout>
        <div>
          <link
            rel="stylesheet"
            href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css"
          />
          <h3>Dream Stories on Sale</h3>
          <Button
            floated="right"
            content="Create a Story"
            icon="add"
            primary={true}
          />
          { this.renderStories() }
        </div>
      </Layout>
(생략)


Footer

Footer는 총 DreamStory 컨트랙트 개수를 표시하려고 합니다. 아직 index.js에 DreamStory 컨트랙트 정보를 읽어오는 부분을 구현하지 않았기 때문에 일단 footer.js라는 파일을 만들고 Layout.js에 추가하였습니다. text 형태로 표시만 하였습니다.


layout.js 수정

Header 파일과 Footer 파일이 준비되었기에 이제 layout..js 파일을 다음과 같이 수정합니다. 그러면 이제 원하는 Header와 Footer가 표시됩니다.

import React from 'react';
import { Container } from 'semantic-ui-react';
import Header from './header';
import Footer from './footer';

export default ( props ) => {
  return (
    <Container>
      <Header />
      {props.children}
      <Footer />
    </Container>
  );
};

image.png



오늘의 실습: index 페이지에 꼭 있어야 할 컴포넌트는 또 뭐가 있을까요?