[Pro react] 6장. 라우팅

URL은 웹이 네이트브 앱에 비해 갖는 중요한 장점이다.URL은 서버에 있는 문서를 가리키는 간단한 포인터의 개념으로 시작했지만 웹 애플리케이션에서는 애플리케이션의 현재 상태를 나타낸다고 할 수 있다. URL을 보면 현재 애플리케이션의 어떤 부분에 있는지 알 수 있으며, 복사해도 나중에 이용하거나 다른 사용자에게 전달 할 수 있다.

라우팅을 구현하는 단순한 방식

기본적인 라우팅 작동방식을 이해하고 기본적인 비중첩 탐색보다 복잡한 시나리오에서 어떤 문제가 발생하는지 알아보기 위해 현재 URL을 기준으로 각기 다른 자식 컴포넌트를 렌더링하는 간단한 컴포넌트를 구현해보자. 깃허브 API를 이용해 프로 리액트 사용자에게 지포지토리 리스트를 반환하는 예제를 작성해보자. 이 어플에서는 “리포지토리”섹션외에도 홈 페이지와 정보섹션을 포함한다.


import React, {Component} from 'react';
import {render} from 'react-dom';

import About from './About';
import Repos from './Repos';
import Home from './Home';


class App extends Component {


    constructor() {
        super(...arguments);
        this.state = {
            route: window.location.hash.substr(1)
        };
    }


    componentDidMount() {
        window.addEventListener('hashchange', () => {
            this.setState({
                route: window.location.hash.substr(1)
            });
        });
    }

    render() {
        var Child;
        switch (this.state.route) {
            case '/about':
                Child = About;
                break;
            case '/repos':
                Child = Repos;
                break;
            default:
                Child = Home;
        }

        return (
            <div>
                <header>App</header>
                <menu>
                    <ui>
                        <li><a href="#/about">About</a></li>
                        <li><a href="#/repos">Repos</a></li>
                    </ui>
                </menu>
                <Child/>
            </div>
        )
    }
}
render(<App />, document.getElementById('root'));


import React, { Component } from 'react';

class About extends Component {
  render() {
    return (
      <h1>Home</h1>
    );
  }
}

export default About;


import React, { Component } from 'react';

class Home extends Component {
  render() {
    return (
      <h1>HOME</h1>
    );
  }
}

export default Home;


import React, {Component} from 'react';

class Repos extends Component {

    render() {
        return (
            <h1>Github Repos</h1>
        )
    }
}

export default Repos;

이 예제 시나리오는 작동에 문제가 없지만 이 방식에는 개념적인 측면과 실용적인 측면에서 적어도 두 가지 걱정이 있다.

이런 방식보다

리액트 라우터를 사용해보자.

리액트 라우터는 리액트 어플에 라우팅을 추가하는 데 가장 많이 사용되는 솔루션으로서 컴포넌트를 중첩 단계에 관계없이 라우트와 연결하는 방법으로 UI를 URL과 동기화 한다. 사용자가 URL을 변경하면 컴포넌트가 자동으로 언마운트 및 마운트 된다. 리액트 라우터 라이브러리의 다른 장점은 사용자가 프로그래밍 방식으로 상태에 진입했든지 아니면 새로운 URL을 입력했든지 관계없이 다른 진입점을 이용하지 않고도 어플의 흐름을 제어하는 메커니즘을 제공한다는 점이다. 즉, 실행되는 코드는 동일하다.

리액트 라우터는 외부 라이브러리이므로 리액트 라우터 피어의 의존성인 히스토리 라이브러리와 함께 npm을 설치해야한다.

npm install –save react-router@3.x.x history@3.x 명령을 이용해 설치 가능하다.

Router, Route 라우트를 선언적으로 어플리케이션의 화면 계층과 매핑하는 데 이용한다.

Link 올바른 href로 완전 접근이 가능한 앵커 태그를 만드는데 이용한다.

 import React, {Component} from 'react';
import {render} from 'react-dom';
import {Router, Route, Link} from 'react-router'

import About from './About';
import Repos from './Repos';
import Home from './Home';


class App extends Component {
    render() {
        return (
            <div>
                <header>App</header>
                <menu>
                    <ui>
                        <li><Link to="/about">About</Link></li>
                        <li><Link to="/repos">Repos</Link></li>
                    </ui>
                </menu>
                {this.props.children}
            </div>
        );
    }
}
render((<Router>
    <Route path="/" component={App}>
        <Route path="about" component={About}/>
        <Route path="repos" component={Repos}/>
    </Route>
</Router>), document.getElementById('root'));

여기서 팁!!!

명명된 컴포넌트: 일반적으로 라우트 하나는 부모 컴포넌트에서 this.props.children을 통해 이용할 수 있는 컴포넌트를 하나 포함한다. 그런데 라우트를 설정할 때 명명된 컴포넌트를 하나 포함한다. 그런데 라우트를 설정할 때 명명된 컴포넌트를 하나 이상 설정할 수 있다.

 React.render((
   <Router>
     <Route path="/" component={App}>
         <Route path="groups" component={content: Groups, sidebar: GroupsSidebar}}/>
         <Route path="users" component={content: Users, sidebar: UserSidebar}}/>
     </Route>
   <Router>
 ), element);

 render(){
   return(
     <div>
     {this.props.children.sidebar} - {this.props.children.content}
     </div>  
   )
 }

인덱스 라우터

테스트해보면 모든 것이 예상대로 동작하지만 원래 구현과 한가지 차이가 있다.

IndexRouter를 설정하자

import React, {Component} from 'react';
import {render} from 'react-dom';
import {Router, Route, Link, IndexRoute} from 'react-router'

import About from './About';
import Repos from './Repos';
import Home from './Home';


class App extends Component {
    render() {
        return (
            <div>
                <header>App</header>
                <menu>
                    <ui>
                        <li><Link to="/about">About</Link></li>
                        <li><Link to="/repos">Repos</Link></li>
                    </ui>
                </menu>
                {this.props.children}
            </div>
        );
    }
}
render((<Router>
    <Route path="/" component={App}>
        <IndexRoute component={Home}/>
        <Route path="about" component={About}/>
        <Route path="repos" component={Repos}/>
    </Route>
</Router>), document.getElementById('root'));

매개변수를 이용하는 라우터

이제 원래의 단순한 라우팅 과 기능상으로 동등한 구현을 만들었다. 이를 확장해 Repos컴포넌트가 실제로 깃허브API를 이용해 데이터를 가져오게 만들 차례다.

속성 전달하기

이 기능을 활용해 About 컴포넌트가 라우터에서 속성을 통해 제목을 받도록 만들어 보자.

import React, {Component} from 'react';
import {render} from 'react-dom';
import {Router, Route, Link, IndexRoute} from 'react-router'


import About from './About';
import Repos from './Repos';
import Home from './Home';
import RepoDetails from './RepoDetails';


class App extends Component {
    render() {
        return (
            <div>
                <header>App</header>
                <menu>
                    <ui>
                        <li><Link to="/about">About</Link></li>
                        <li><Link to="/repos">Repos</Link></li>
                    </ui>
                </menu>
                {this.props.children}
            </div>
        );
    }
}
render((<Router>
    <Route path="/" component={App}>
        <IndexRoute component={Home}/>
        <Route path="about" component={About} title="About us"/>
        <Route path="repos" component={Repos}>
            <Route path="details/:repo_name" component={RepoDetails}/>
        </Route>
    </Route>
</Router>), document.getElementById('root'));

import React, { Component } from 'react';

class About extends Component {
  render() {
    return (
      <h1>{this.props.route.title}</h1>
    );
  }
}

export default About;

자식 복제와 속성 주입

속성이 동직인 경우 리액트 라우터가 속성으로서 주입하는 child 컴포넌트를 복제하는 방법이 아주 유용하다. 이 방법을 이용하면 추가 속성을 전달 할 수 있는 기회가 있다.

칸반 앱: 라우팅

지금까지 작성한 칸반 애플리케이션은 예제로는 문제가 없지만 카드를 편집하거나 새로 만드는 기능이 없기 때문에 실ㄹ제로 사용할 수는 없다. 다음은 라우트를 이용해 카드 작성과 편집기능을 추가해보자. /new 라우트는 새로운 카드를 작성하는 폼을 표시하고, /edit/:card_id라우트는 카드의 현재 속성을 확인하고 편집할 수 있는 폼을 표시하기로 한다. 이를 위해 NewCard와 EditCard 컴포넌트를 새로 만들어야 하는데 두 컴포넌트는 많은 특징을 공유하므로 두 컴포넌트에서 모두 이용되며 공유되는 UI를 모두 포함하는 CardForm 컴포넌트를 따로 만들어보자.

여러 다른 파일을 이용해 작업해야 하므로 필요한 작업 내용을 간단히 정리해보자.

 import React,{Component, PropTypes} from 'react';
import CardForm from './CardForm'

class NewCard extends Component{

    componentWillMount(){
        this.setState({
            id: Date.now(),
            title:'',
            description:'',
            status:'todo',
            color:'#c9c9c9',
            tasks:[]
        });
    }

    handleChange(field, value){
        this.setState({[field]: value});
    }

    handleSubmit(e){
        e.preventDefault();
        this.props.cardCallbacks.addCard(this.state);
        this.props.history.pushState(null,'/');
    }

    handleClose(e){
        this.props.history.pushState(null,'/');
    }

    render(){
        return (
            <CardForm draftCard={this.state}
                      buttonLabel="Create Card"
                      handleChange={this.handleChange.bind(this)}
                      handleSubmit={this.handleSubmit.bind(this)}
                      handleClose={this.handleClose.bind(this)} />
        );
    }
}

NewCard.propTypes = {
    cardCallbacks: PropTypes.object,
};


export default NewCard;

완성코드는 해당 Repo에 첨부하겠습니다.