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
'리액트' 카테고리의 다른 글
create-react-app으로 react app을 만들었을 때 한글 깨짐 현상 해결 (0) | 2021.04.07 |
---|---|
#11. 리액트와 서버 (0) | 2021.04.06 |
#9. 테스팅(2) - 스냅샷 테스팅, 코드 커버리지 (0) | 2021.04.02 |
#9. 테스팅(1) - ESLint, Redux와 React Component 테스트하기 (0) | 2021.04.02 |
#8. React-Redux (0) | 2021.04.01 |