Dynamic Routing

이전 장에서 new_story 페이지를 완성했었습니다. 그러나 엄밀히 완성은 아닙니다. 페이지는 꾸며져서 동작하나 헤더의 다른 페이지로 이동하는 것이 구현되지 않습니다. 또 헤더의 네비게이션 기능이 미구현 상태입니다.

오래전에 다음과 같은 라우팅 맵을 보여주었습니다.


가만 보면, 라우팅에 컨트랙트 주소가 들어가 있습니다. 즉, 동적으로 라우팅을 해야 합니다. 불행하게도 Next.js는 기본적으로 동적 라우팅을 지원하지 않습니다. 따라서 몇 가지 고생을 해야 합니다.


사용자 라우팅 설정

고생한다는 것은 Next의 기본 라우팅 방식을 사용하지 않고, 사용자 입맛에 맛게 라우팅하기 위한 작업을 해야 한다는 뜻입니다. 


next-routes 설치

Next에서 동적 라우팅을 하기 위해서는 깃허브에서 next-routes 패키지(https://github.com/fridays/next-routes)를 설치해야 합니다.

$ npm install --save next-routes


자세한 내용보다는 사용하는 방법 위주로 설명하겠습니다. 자세한 내용이 보고 싶은 분은 next-routes 페이지를 방문해 보세요.

이 패키지를 설치하고 나서 다음 2개의 파일을 별도로 만들어야 합니다.

  • routes.js: 정적 라우트를 포함하여 동적 라우트들을 기술한 파일
  • server.js: Next가 부팅될 때, Next에게 routes.js를 이용하라고 알려주는 역할


이 두 파일 모두 프로젝트의 루트에 생성합니다. routes.js는 라우트들을 기술하기도 하지만, 다른 역할도 하는데, 그것은 동적 라우팅에 용이하도록 몇 가지 Helper 모듈을 제공합니다.


routes.js 생성

그럼 먼저 routes.js 파일을 작성합니다. 생성 위치는 프로젝트 root 폴더입니다.

// the require returns a function,
// so we need to () to invoke the function
const routes= require( 'next-routes' )();

// export some helpers
module.exports= routes;

우선은 별 내용이 없습니다. 동적 라우팅을 위한 내용은 나중에 넣도록 하겠습니다. 우선 routes.js를 이용하여 페이지간의 네비게이션을 보여줄 것입니다.
routes.js 코드에서 next-routes 라이브러리를 사용할 때 마지막에 ()가 들어가는 것을 주의하세요. next-routes의 반환 결과가 함수가 되어 함수를 실행시키기 위해 ()가 필요합니다.


server.js 생성

server.js 코드는 다음과 같습니다. 그런데 server.js 코드도 동적 라우팅 부분이 나중에 추가될 예정이라 다음 코드에 많은 변화가 있을 것입니다. 그러나 일단 next의 기본 라우팅이 아니라 사용자 라우팅을 테스트하려고 합니다.

// get createServer from http library
const { createServer }= require( 'http' );
// get next library
const next= require( 'next' );

// run the server as dev mode.
const app= next({
  // so check if production mode
  dev: process.env.NODE_ENV !== 'production'
});

// import routes that we made
const routes= require( './routes' );
// get handler
const handler= routes.getRequestHandler( app );
// setup app
app. prepare().then( () => {
  createServer( handler ).listen( 3000, (error)  => {
    if( error ) throw error;
    console.log( "Ready on localhost:3000" );
  });
});


pakage.json 수정

기억하실지 모르겠지만 Next로 웹서버를 기동하는 명령 npm run dev입니다. 이렇게 하면 Next의 기본 라우팅 방식이 사용됩니다. 사용자 라우팅을 적용하려면 package.json 파일에서 해당 부분을 사용자 라우팅이 실행되도록 수정해야 합니다.


scripts의 "dev": "next dev"라고 되어 있는 것을 이 그림처럼 바꿉니다. 그러면 npm run dev 실행했을 때 우리가 만든 server.js 파일이 실행됩니다. 결과적으로 routes.js가 실행되게 됩니다.


사용자 라우팅 테스트

그럼 기존에 Next 서버를 중지합니다. 그런 후 다시 실행합니다.

$ npm run dev


조금전에 package.json 파일에서 이 명령을 실행했을 때 우리가 설정한 server.js가 실행됩니다. 결과적으로는 차이가 없습니다. 아직 동적 라우팅을 하지 않았기 때문입니다.

간단한 테스트로 new_story 페이지에서 "Create" 버튼으로 DreamStroy가 만들어진 후 index 페이지로 자동으로 이동하는 기능을 넣어 보겠습니다. new_story에서 Create가 제대로 수행된 직후에 Router.pushRoute( '/' );를 넣어주면 됩니다. 이 기능을 사용하기 위해 작성한 routes.js에서 헬퍼인 Router를 불러야 합니다. new_story.js 파일에 아래와 같이 해당 내용을 추가합니다.

// import route helper
import { Router } from '../../routes';
(생략)
      await dream_factory.methods
      .createDreamStory( min_down_price_wei, this.state.title, this.state.story )
      .send({
        from: accounts[0]
      });
      // redirect to the index page
      Router.pushRoute( '/' );
    } catch (error) {
      this.setState( { error_msg: error.message } );
    }


이렇게 한 후 new_story 페이지에서 DreamStory 생성이 정상적으로 되면 컨트랙트가 블록체인에 배포된 직후, index 페이지로 이동하게 됩니다.


Header 네이게이션

그럼 사용자 라우팅 방식으로 모든 페이지 상단에 위치하는 헤더의 네비게이션 기능을 구현해 보겠습니다. 현재 Header는 Menu.Item으로 꾸며져 있는데, 이것들을 라우팅이 될 수 있도록 링크로 만들겠습니다.

header.js 파일의 내용을 다음과 같이 변경합니다.

// import react library
import React from 'react';
import { Menu, Input } from 'semantic-ui-react';
// import Link from routes.js
import { Link } from '../routes';

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

import { Link } from '../routes';에서는 routes의 Link 헬퍼를 사용하고 있습니다. 그리고 Menu.Item으로 된 것을 <Link>태그로 감쌌습니다. 사실 <Link>태크는 페이지 이동을 담당하지 않고, 이벤트 핸들러를 연결하는 기능만 하기 때문에 실제 페이지 이동을 위해  <a>태그 사용이 필요합니다.

Header의 "DreamChain"을 누르거나 "Dream Stories"를 누르면 index 페이지로 이동하게 됩니다. "+" 버튼을 누르면 새롭게 DreamStory를 작성할 수 있는 new_story 페이지로 이동합니다. 이와 같이 Header를 구현한 후 브라우저를 새로고침하면 헤더의 링크가 활성화되는 것을 볼 수 있습니다. 링크를 포인팅했을 때 아이콘이 변경되며, 색깔이 약간 회색으로 바뀝니다.


인터넷 접속

브라우저가 로컬 서버에 접속해서 도는 것이지만 다음과 같은 에러 메지가 나타날 때가 있습니다.


알아보니 인터넷 접속이 필요합니다. 그 이유는 서버 실행할 때, web3.js에서 Infura API를 통해 provider를 구성하는 부분에서 인터넷 접속이 필요합니다. 웹서버 실행할 때 web3 provider가 인터넷 접속이 안 될 경우 제대로 설정되지 않아 에러가 발생한 거 같습니다. 혹시 유사한 에러가 발생하면 인터넷 연결을 확인해 보세요. 로컬에서 Front-end 구현을 하고 있지만 Rinkeby 테스트넷에 접속하여 스마트 컨트랙트와 인터페이싱 해야하므로 인터넷 접속이 필요합니다.


story_details 임시 페이지

먼저 index 페이지에 나타난 DreamStory 컨트랙트 주소를 클릭했을 때 나타낼 story_details 페이지를 임시적으로 다음과 같이 꾸밉니다. 많은 내용을 추가해야 해서 일단은 간단히 페이지를 만들고 다음에 내용을 채우겠습니다.

// import react
import React, { Component } from 'react';
// import layout
import Layout from '../../components/layout';

// class based component
class StoryDetails extends Component {
  render() {
    return (
      <Layout>
        <h3>Story Details</h3>
      </Layout>
    )
  }
}

// export the component
export default StoryDetails;


동적 routes 맵핑

이제 동적 라우팅 맵핑을 위해 routes.js 파일을 다음과 같이 수정합니다.

// the require returns a function,
// so we need to () to invoke the function
const routes= require( 'next-routes' )();

// define a new route mapping using add() function with pattern
routes.add( '/dream_stories/:address', '/dream_stories/story_details' );

// export some helpers
module.exports= routes;


routes.add( '/dream_stories/:address', '/dream_stories/story_details' ); 부분을 유심히 보세요. routes의 add라는 함수를 이용하여 새로운 라우트 맵핑을 추가하고 있습니다. add 함수의 첫 번째 인자는 라우트의 패턴입니다. 즉, 동적으로 생성하는 라우트의 와일드 카드 방식으로 패턴을 지정합니다. :address는 index 페이지에서 전달되도록 할 것입니다.

두 번째 인자는 해당 라우트가 나타낼 페이지입니다. 여기서는 좀 전에 만든 story_details 페이지를 설정합니다. 이때 pages 폴더 기준으로 경로까지 같이 입력합니다.


index 페이지 수정

이제 index 페이지에서 "View Story Details"를 클릭했을 때, 컨트랙트 주소를 routes에 전달하기만 하면 됩니다. 다음과 같이 index.js 파일을 수정합니다.

(생략)
// 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: (
          <Link route={`/dream_stories/${address}`}>
            <a>View Story Details</a>
          </Link>
        ),
        meta:'5,000',
        fluid: true
      };
    });

    return <Card.Group items= {items} />;
  }
(생략)

수정된 부분은 <Link route={/dream_stories/${address}}>입니다. <a>태그를 <Link>태그로 감싸고, 라우트에 컨트랙트 주소를 전달하고 있습니다.

동적 라우팅 구현이 다 됐습니다. 그럼 테스트하겠습니다. 브라우저로 가서 새로고침을 합니다. 그런 후 "View Story Details"에 마우스를 올려 봅니다. 그러면 다음과 같이 브라우저 하단에 이동할 링크가 보입니다.


클릭하면 story_details 페이지로 이동합니다. 이렇게요.


routes 추가 수정

그런데 헤더의 "+"을 클릭하면 new_story 페이지로 이동하지 않습니다. index 페이지에서 클릭해도 똑같습니다. 다음과 같이 show_details 페이지가 표시될 뿐입니다.


이유는 routes 파일에서 설정한 패턴 때문입니다. new_story 페이지의 링크는 http://localhost:3000/dream_stories/new_story 이것입니다. 그런데 routes 파일에 구현된 내용을 보면 이 링크는 패턴이 일치합니다. 즉, new_story 페이지로 이동하라는 명령이 패턴으로 인식되어 story_details 페이지가 나타난 것입니다.

여기가 routes.add( '/dream_stories/:address', '/dream_stories/story_details' ); 문제가 되는 부분인데 의외로 해결 방법은 간단합니다. routes 맵핑을 추가하면 됩니다. 그러나 주의할 것은 패턴에 대한 맵핑보다 먼저 오게끔 추가해야 합니다. 만약 패턴 라우팅 뒤에 오게 되면 같은 문제가 발생합니다. routes.js 파일을 다음과 같이 수정합니다.

// define a new route mapping using add() function with pattern
// the priority route should come first
routes
  .add( '/dream_stories/new_story', '/dream_stories/new_story' )
  .add( '/dream_stories/:address', '/dream_stories/story_details' );

추가된 부분은 .add( '/dream_stories/new_story', '/dream_stories/new_story' )입니다. 패턴 라우팅 위에 추가된 것이 보입니다. new_story라는 링크를 클릭하면 new_story 페이지를 표시하라는 내용을 추가한 것입니다.

이렇게 한 후 다시 브라우저로 가서 new_story 페이지로 이동해 봅니다. 제대로 표시되는 것을 알 수 있습니다.


마지막으로 한가지 더 해줘야 할 게 있습니다. new_story 페이지에서 새로고침을 하면 다시 story_details 페이지가 나타납니다. 라우트 맵핑이 바뀌었기 때문에 웹서버를 다시 실행해야 합니다. root 폴더로 이동하여 다음 명령을 실행합니다.

$ npm run dev


아마 뒤에 다운로드 페이지 표시관련해서 몇 가지 라우트 맵핑이 추가될 것입니다. 그때마다 웹서버를 다시 실행해야 하는 걸을 잊지 마세요.


Misc

몇 가지 추가적으로 수정한 것이 있습니다. 이미 앞의 그림에 드러났지만, index 페이지에 Story의 개수를 Footer에 표시했습니다. 방법은 DreamFactory 인스턴스가 불러들인 stories 인스턴스를 이용하여 단순히 lengh property를 이용했습니다.

(생략)
          </Link>
          { this.renderStories() }
          <h3>The number of Stories: {this.props.stories.length}</h3>
        </div>
(생략)


또, Footer를 그동안 의미없게 놔뒀는데, 조금 의미를 부여해 봤습니다.

Share your dream story and earn dream coins
Footer는 필요에 맞게 적절히 사용하면 되겠습니다. 나중에 더 중요하게 쓰일지도 모르겠네요.


이상으로 동적 라우팅을 구현해 봤습니다. 그렇게 복잡하지 않아서 좋네요. 웹페이지를 꾸며가면서 느끼는 점은 좀 더 예쁘게 꾸며 보고 싶은데, 아직 척척 꾸미기엔 실력이 모자람을 느낍니다. 그래도 꾸며가는 재미에 배워가는 재미가 있어서 좋습니다. 그렇죠?



오늘의 실습: Footer를 어떻게 활용하면 좋을까요?



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