리액트

#10. React Router

YJH3968 2021. 4. 4. 22:35
728x90
  • 단일 페이지 앱에서는 한 페이지 안에서 모든 일이 벌어지기 때문에 브라우저 방문 기록, 책갈피, 이전 페이지, 다음 페이지 등의 기능은 적절한 routing 솔루션이 없으면 제대로 작동하지 않는다.
  • routing이란 클라이언트의 요청을 처리할 endpoint를 찾는 과정이다. endpoint는 브라우저의 위치, 방문 기록 객체와 함께 작동해 요청받은 content를 식별한다. javascript는 이렇게 식별한 content를 가져와서 적절히 UI를 렌더링할 수 있다.
  • react router는 리액트 앱을 위한 routing 솔루션이다.

 

1. router 사용하기

  • router를 사용하면 웹사이트의 각 section에 대한 경로를 설정할 수 있다. 각 경로는 브라우저의 주소창에 넣을 수 있는 endpoint를 뜻한다. 앱은 요청받은 경로에 따라 적절한 content를 렌더링해 보여준다.
  • react-router-dom은 단일 페이지 앱의 navigation 이력을 관리할 수 있는 다양한 방법을 제공한다.
  • HashRouter는 클라이언트를 위해 설계된 router로 주소창의 현재 페이지 경로 뒤에 '#' 식별자를 입력하면 브라우저는 서버 요청을 보내지 않고 현재 페이지에서 식별자에 해당하는 id attribute가 있는 element를 찾아서 그 element의 위치를 화면에 보여준다.
// react-router-dom 설치
npm install react-router-dom --save
  • 이제 사이트맵 상의 각 페이지(또는 메뉴 항목)에 해당하는 내용을 담을 component를 만든다.
// 각 페이지에 해당하는 내용을 담을 component
export const Home = () =>
  <section className="home">
    <h1>[홈페이지]</h1>
  </section>

export const About = () =>
  <section className="about">
    <h1>[회사 소개]</h1>
  </section>

export const Events = () =>
  <section className="events">
    <h1>[이벤트]</h1>
  </section>

export const Products = () =>
  <section className="products">
    <h1>[제품]</h1>
  </section>

export const Contact = () =>
  <section className="contact">
    <h1>[고객 지원]</h1>
  </section>
  • 이 애플리케이션은 시작 시 App component 대신 HashRouter component를 렌더링한다.
// HashRouter component 렌더링
import React from 'react'
import { render } from 'react-dom'

import { HashRouter, Route } from 'react-router-dom'

import { Home, About, Events, Products, Contact } from './pages'

window.React = React

render(
  <HashRouter>
    <div className="main">
      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
      <Route path="/events" component={Events} />
      <Route path="/products" component={Products} />
      <Route path="/contact" component={Contact} />
    </div>
  </HashRouter>,
  document.getElementById('react-container')
)
  • HashRouter component는 애플리케이션의 경로 component로 렌더링되고 각 경로는 Route component로 정의된다.
  • 이렇게 정의한 Route와 윈도우의 주소에 따라 router가 렌더링할 component가 결정된다. Router component에는 path와 component 프로퍼티가 있는데, 주소가 path와 일치하면 component가 표시된다.
  • exact 프로퍼티는 주소가 path와 정확히 일치할 때만 component를 표시한다는 뜻이다.
  • 하지만 사용자는 직접 주소를 타이핑하면서 사이트를 navigate하지 않기 때문에, react-router-dom은 브라우저 링크를 만들어주는 Link라는 component를 제공한다.
// link 사용 예제
import { Link } from 'react-router-dom'

export const Homt = () =>
  <div className="home">
    <h1>[홈페이지]</h1>
    <nav>
      <Link to="about">[회사 소개]</Link>
      <Link to="events">[이벤트]</Link>
      <Link to="products">[제품]</Link>
      <Link to="contact">[고객 지원]</Link>
    </nav>
  </div>
  • react router는 렌더링하는 component에 프로퍼티를 넘기는데, 예를 들어 현재 위치를 프로퍼티에서 얻을 수 있다.
// 정의되지 않은 경로에 해당하는 주소를 주소창에 입력한 경우에 렌더링된다.
export const Whoops404 = ({ location }) =>
  <div className="whoops-404">
    <h1>'{location.pathname}' 경로의 자원을 찾을 수 없습니다.</h1>
  </div>

// 위 component를 경로 객체에 프로퍼티로 넘긴다.
import {
  HashRouter,
  Route,
  Switch  
} from 'react-router-dom'

...

render(
  <HashRouter>
    <div className="main">
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/events" component={Events} />
        <Route path="/products" component={Products} />
        <Route path="/contact" component={Contact} />
        <Route component={Whoops404} />
      </Switch>
    </div>
  </HashRouter>,
  document.getElementById('react-container')
)
  • Switch component는 경로가 일치하는 Route에서 첫 번째 경로를 렌더링한다. 따라서 여러 Route 중 오직 하나만 렌더링하도록 보장할 수 있다.
  • 만약 경로와 일치하는 Route가 하나도 없다면 path 프로퍼티가 없는 맨 마지막 Route가 표시된다.

 

2. 경로 내포시키기

  • 사용자가 웹앱을 navigate하더라도 앱의 UI 중 일부가 계속 같은 위치에 남아 있기를 원하는 경우가 있다. 이는 children 프로퍼티를 이용하면 리액트 component를 템플릿으로 자연스럽게 합성할 수 있다.
// MainMenu component
import HomeIcon from 'react-icons/lib/fa/home'
import { NavLink } from 'react-router-dom'
import './stylesheets/menus.scss'

const selectedStyle = {
  backgroundColor: "white",
  color: "slategray"
}

export const MainMenu = () =>
  <nav className="main-menu">
    <NavLink to="/">
      <HomeIcon />
    </NavLink>
    <NavLink to="/about" activeStyle={selectedStyle}>
      [회사 소개]
    </NavLink>
    <NavLink to="/events" activeStyle={selectedStyle}>
      [이벤트]
    </NavLink>
    <NavLink to="/products" activeStyle={selectedStyle}>
      [제품]
    </NavLink>
    <NavLink to="/contact" activeStyle={selectedStyle}>
      [고객 지원]
    </NavLink>
  </nav>
  • NavLink component는 링크가 활성화된 경우 다른 스타일로 표시되는 링크를 만들 때 사용할 수 있다. activeStyle 프로퍼티를 사용해 해당 링크가 선택되었거나 활성화되었음을 표시할 떄 사용할 CSS를 지정할 수 있다.
// MainMenu component는 PageTemplate component 안에 들어간다.
import { MainMenu } from './ui/menus'

...

const PageTemplate = ({children}) =>
  <div className="page">
    <MainMenu />
    {children}
  </div>
  • PageTemplate component에 있는 children은 각 section이 렌더링될 부분이다. 여기서는 MainMenu 뒤에 그냥 children을 넣었다. 이제 PageTemplate을 사용해 각 section을 합성할 수 있다.
// PageTemplate를 사용해 각 section을 합성할 수 있다.
export const Events = () =>
  <PageTemplate>
    <section className="events">
      <h1>[이벤트]</h1>
    </section>
  </PageTemplate>

export const Products = () =>
  <PageTemplate>
    <section className="products">
      <h1>[제품]</h1>
    </section>
  </PageTemplate>

export const Contact = () =>
  <PageTemplate>
    <section className="contact">
      <h1>[고객 지원]</h1>
    </section>
  </PageTemplate>

export const About = ({ match }) =>
  <PageTemplate>
    <section className="about">
      <h1>[회사 소개]</h1>
    </section>
  </PageTemplate>
  • 이 애플리케이션을 실행하면 각 section이 같은 MainMenu를 표시하고 내부 페이지를 navigate하면 화면 오른쪽의 content가 바뀐다.
  • '회사 소개'의 하위 메뉴를 만들기 위해 NavLink component를 사용하고 activeStyle을 MainMenu의 activeStyle과 동일하게 만든다.
// '회사 소개'의 하위 메뉴
export const AboutMenu = ({match}) =>
  <div className="about-menu">
    <li>
      <NavLink to="/about" style={match.isExact && selectedStyle}>
        [회사]
      </NavLink>
    </li>
    <li>
      <NavLink to="/about/history" activeStyle={selectedStyle}>
        [연혁]
      </NavLink>
    </li>
    <li>
      <NavLink to="/about/services" activeStyle={selectedStyle}>
        [서비스]
      </NavLink>
    </li>
    <li>
      <NavLink to="/about/location" activeStyle={selectedStyle}>
        [위치]
      </NavLink>
    </li>
  </div>
  • AboutMenu component의 하위 component는 routing 관련 프로퍼티를 전달받는데, 그 프로퍼티 중 Route에서 이 component로 전달하는 match 프로퍼티를 사용한다.
  • 다른 component는 activeStyle 프로퍼티를 사용하는 반면, 첫 번째 NavLink component는 match.isExact가 true인 경우에만 selectedStyle이 style 프로퍼티로 설정되도록 설정한다. match.isExact는 위치가 정확히 /about인 경우에만 true다.
// About component에 대한 경로를 추가한다.
export const About = ({ match }) =>
  <PageTemplate>
    <section className="about">
      <Route component={AboutMenu} />
      <Route exact path="/about" component={Company} />
      <Route path="/about/history" component={History} />
      <Route path="/about/services" component={Services} />
      <Route path="/about/location" component={Location} />
    </section>
  </PageTemplate>
  • 윈도우의 경로는 애플리케이션이 어떤 하위 section을 렌더링할지 결정한다. 예를 들어 http://localhost:3000/about/history라는 경로는 About component 아래의 History component를 렌더링한다.
  • 여기서는 Switch component를 사용하지 않는다. 윈도우 경로와 일치하는 Route가 component attribute에 지정된 component를 렌더링한다. 첫 번째 Route는 항상 AboutMenu를 출력한다.
  • 경우에 따라 한 경로를 다른 경로로 redirection해야 할 수도 있다. 예를 들어 http://localhost:3000/services에 대한 요청을 제대로 된 http://localhost:3000/about/Services 경로로 redirection할 수 있다.
// redirection 예제
import {
  HashRouter,
  Route,
  Switch,
  Redirect
} from 'react-router-dom'

...

render(
  <HashRouter>
    <div className="main">
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Redirect from="/history" to="/about/history" />
        <Redirect from="/services" to="/about/services" />
        <Redirect from="/location" to="/about/location" />
        <Route path="/events" component={Events} />
        <Route path="/products" component={Products} />
        <Route path="/contact" component={Contact} />
        <Route component={Whoops404} />
      </Switch>
    </div>
  </HashRouter>,
  document.getElementById('react-container')
)
  • 이러한 redirection 기능은 사용자가 예전 경로를 통해 예전 content에 접근하는 경우 Redirect component를 사용함으로써 제대로 된 content를 제공하기에 유용하다.

 

3. Router Parameter

  • router parameter는 URL에서 값을 얻을 수 있는 변수로 content를 걸러내거나 사용자 선호에 따라 여러 표시 방법을 제공해야 하는 데이터 주도 웹 애플리케이션에 유용하다.
  • react router를 사용해 색 관리 앱에 한 번에 한 색을 선택해 표시하는 기능을 추가한다.
// id field로 객체를 찾는 findById 함수
import { compose } from 'redux'

export const getFirstArrayItem = array => array[0]

export const filterArrayById = (array, id) =>
  array.filter(item => item.id === id)

export const findById = compose(
  getFirstArrayItem,
  filterArrayById
)
  • router를 사용하면 URL에서 색 ID를 얻을 수 있는데, 여기에 router parameter를 사용한다. router parameter는 콜론(:)으로 시작한다. 다음은 URL의 맨 마지막 부분을 Route 안에 id라는 이름의 parameter로 저장하고 이를 출력하는 예제다.
// router parameter를 지정하는 예제
<Route exact path="/:id" component={UniqueIDHeader} />

const UniqueIDHeader = ({match}) => <h1>{match.params.id}</h1>
// 사용자가 어떤 색을 선택한 경우 렌더링해야 하는 ColorDetails component
const ColorDetails = ({ title, color }) =>
  <div className="color-details" style={{backgroundColor: color}}>
    <h1>{title}</h1>
    <h1>{color}</h1>
  </div>
  • ColorDetails component는 표현 component이기 때문에 자세한 색 정보를 프로퍼티로 넘겨줘야 한다. 색 관리 앱이 redux를 사용하고 있기 때문에 이는 Color라는 container를 새로 만들어 구현한다.
// router parameter로부터 가져온 사용자가 선택한 색을
// 상태에서 찾아내 표현 component에 전달해주는 Color container
export const Color = connect(
  (state, props) => findById(state.colors, props.match.params.id)
)(ColorDetails)
  • connect HOC를 사용해 Color container를 만든다. connect의 첫 번째 인자는 상태에서 가져온 색을 가지고 ColorDetails의 프로퍼티를 설정하는 함수다. findById를 통해 찾은 색 객체의 데이터를 ColorDetails의 프로퍼티로 만들어준다.
  • connect HOC는 Color container에 전달된 모든 프로퍼티를 ColorDetails component의 프로퍼티로 변환하기 때문에 router가 전달한 모든 프로퍼티가 ColorDetails component에 전달된다.
// history.goBack() method는 이전에 사용자가 봤던 위치를 다시 표시한다.
const ColorDetails = ({ title, color }) =>
  <div className="color-details" style={{backgroundColor: color}}
      onClick={() => history.goBack()}>
    <h1>{title}</h1>
    <h1>{color}</h1>
  </div>
  • 이제 Color container를 앱에 추가한다.
// App component를 처음 렌더링할 떄는 이 component를 HashRouter로 감싸야 한다.
import { HashRouter } from 'react-router-dom'

...

render(
  <Provider store={store}>
    <HashRouter>
      <App />
    </HashRouter>
  </Provider>,
  document.getElementById('react-container')
)
// App에 경로를 추가한다.
import { Route, Switch } from 'react-router-dom'
import Menu from './ui/Menu'
import { Colors, Color, NewColor } from './containers'
import '../stylesheets/APP.scss'

const App = () =>
  <Switch>
    <Route exact path="/:id" component={Color} />
    <Route path="/" component={() => (
      <div className="app">
        <Menu />
        <NewColor />
        <Colors />
      </div>
    )} />
  </Switch>

export default App
  • 이제 URL에 id가 전달된 경우 Color component를 렌더링하고, 이를 제외한 모든 다른 위치는 /와 match되어 주 애플리케이션 component를 표시한다.
  • 이제 사용자에게 개별 색 페이지로 navigate할 수 있는 수단을 제공해야 개별 색 페이지(Color component)를 쉽게 볼 수 있다.
  • withRouter를 사용하면 component 프로퍼티로 router의 history를 얻을 수 있다. 이 history를 사용해 Color component 안에서 navigation을 제공할 수 있다. 
// Color component
import { withRouter } from 'react-router'

...

class Color extends Component {
  render() {
    const {
      id, title, color, rating, timestamp, onRemove, onRate, history 
    } = this.props
    return (
      <section className="color" style={this.style}>
        <h1 ref="title" onClick={() => history.push(`/${id}`)}>{title}</h1>
        <button onClick={onRemove}><FaTrash /></button>
        <div className="color" onClick={() => history.push(`/${id}`)}
          style=({ backgroundColor: color })>
        </div>
        <TimeAgo timestamp={timestamp} />
        <div>
          <StarRating starsSelected={rating} onRate={onRate} />
        </div>
      </section>
    )
  }
}

export default withRouter(Color)
  • withRouter는 HOC로 Color component를 export하면 withRouter를 호출해 Color component를 wrapping하면서 router 프로퍼티인 match, history, location 등을 전달해준다.
  • history 객체를 직접 사용해 navigation을 얻을 수 있다. 사용자가 색의 제목이나 색 자체를 클릭하면 색의 ID를 포함한 새로운 경로가 history 객체에 push되고 navigation이 일어난다.
  • 색 정렬 상태도 같은 방식으로 router로 옮길 수 있다. 현재 색 목록을 정렬하는 방법은 redux store의 sort 프로퍼티로 들어 있는데, 이를 경로 parameter로 옮기면 된다.
  • 먼저 store 파일에서 sort reducer를 제거한다. 즉, 해당 상태 변수를 더 이상 redux가 관리하지 않는다.
// store 파일의 기존 상태
combineReducers({colors, sort})

// store 파일의 변경 후 상태
combineReducers({colors})
  • 또한 Menu component를 위한 container를 제거할 수 있다. Menu component를 위한 container는 redux store의 상태 중 sort 상태를 Menu 표현 component와 연결해줬었다.
  • container.js 파일의 Colors container는 상태로부터 더 이상 sort 값을 받지 않고 match 프로퍼티 내의 Colors component로 전달되는 경로 parameter로 정렬 방법을 전달받는다.
// 정렬 방벙을 router로 옮겼을 때 Colors container
export const Colors = connect(
  ({colors}, {match}) =>
    ({
      colors: sortColor(colors, match.params.sort)
    }),
  dispatch =>
    ({
      onRemove(id) {
        dispatch(removeColor(id))
      },
      onRate(id, rating) {
        dispatch(rateColor(id, rating))
      }
    })
)(ColorList)
  • 다음으로 Menu component를 수정해서 새로운 경로를 포함하게 만들어야 한다.
// Menu component 수정
import { NavLink } from 'react-router'

const selectedStyle = { color : 'red' }

const Menu = ({ match }) =>
  <nav className="menu">
    <NavLink to="/" style={match.isExact && selectedStyle}>날짜</NavLink>
    <NavLink to="/sort/title" activeStyle={selectedStyle}>이름</NavLink>
    <NavLink to="/sort/rating" activeStyle={selectedStyle}>평점</NavLink>
  </nav>

export default Menu
  • 이제 사용자는 URL을 통해 정렬 방식을 지정할 수 있다.
  • App component도 색 정렬 방법을 경로를 통해 처리하도록 바꿀 필요가 있다.
// App component 수정
const App = () =>
  <Switch>
    <Route exact path="/:id" component={Color} />
    <Route path="/" component={() => (
      <div className="app">
        <Route component={Menu} />
        <NewColor />
        <Switch>
          <Route exact path="/" component={Colors} />
          <Route path="/sort/:sort" component={Colors} />
        </Switch>
      </div>
    )} />
  </Switch>
  • Menu에는 match 프로퍼티가 필요하므로 Route를 사용해 렌더링한다.
  • NewColor component 뒤에는 default 정렬 방법인 날짜로 정렬한 색 목록이 오거나 사용자가 경로 parameter로 지정한 정렬 방법으로 정렬한 색 목록이 온다. 이 두 경로를 Switch component로 감싸서 오직 한 Colors container만 렌더링하게 만들어야 한다.

 

출처 : learning react(2018)

728x90