상품 리스트 페이지에서 해당 상품을 클릭 시 상세 페이지로 넘어가는 기능은 단순히 path를 바꿔준다고 되는 것이 아니다. 리액트의 3rd party 라이브러리를 사용하여 동적라우팅을 구현해보자.
// Routes.js
import React from 'react';
import {
BrowserRouter as Router,
Route,
Switch,
} from 'react-router-dom';
import Home from './Pages/Home';
import List from './Pages/List';
import Detail from './Pages/Detail';
class Routes extends React.Component {
render() {
return (
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/list" component={List} />
<Route exact path="/detail" component={Detail} />
</Switch>
</Router>
)
}
}
export default Routes;
수백개의 상세페이지는 각각 따로 url을 부여하고 만드는 것이 아니라 해당 상품의 id 값으로 그때 그때 컴포넌트를 렌더해서 불러온다고 생각하면 된다. 위 코드에서는 url path로 /detail
설정 시 해당 컴포넌트로 이동하시만 어떤 상품을 보여줄지는 지정이 되지 않는다. 유동 데이터를 사용하면 이런 문제를 해결할 수 있다.
동적 라우팅은 라우트의 경로에 특정 값을 넣어서 해당 페이지로 이동할 수 있게 하는 것이다. React Router에서는 두가지 방법으로 이 기능을 구현할 수 있다.
컴포넌트를 라우트로 설정하게 되면 3가지의 props를 전달받는다.
history
이 객체를 통해서 push
, replace
로 다른 경로로 이동하거나 앞 뒤 페이지로 전환할 수 있다. 컴포넌트 이동시에 this.props.history.push('/page')
를 사용했던 것을 기억하면 된다.
location
이 객체는 현재 경로에 대한 정보를 지니고 있고, URL 쿼리 (/company_list?category=12)에 대한 정보를 가지고 있다.
match
이 객체에는 어떤 라우트에 매칭이 되었는지에 대한 정보가 있고 params(/company_detail/:id)에 대한 정보를 갖고 있다.
예제를 보면서 이해해보자!
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Monsters from "./Assignments/Monsters/Monsters";
import Pagination from "./Lectures/Pagination/Photos";
import UrlParameters from "./Lectures/UrlParameters/Monsters";
import MonsterDetail from "./Lectures/UrlParameters/MonsterDetail";
import StateProps from "./Lectures/StateProps/StateProps";
import OnChange from "./Lectures/OnChange/OnChange";
import MenuTab from "./Lectures/MenuTab/MenuTab";
export default class Routes extends Component {
render() {
return (
<Router>
<Switch>
<Route exact path="/" component={Monsters} />
<Route exact path="/monsters" component={UrlParameters} />
<Route exact path="/monsters/detail" component={MonsterDetail} />
<Route exact path="/monsters/detail/:id" component={MonsterDetail} />
</Switch>
</Router>
);
}
}
index 화면의 List 안에 있는 카드를 클릭 시 해당 monster의 상세 페이지로 넘어가는 형태를 구현해보자. /monsters/detail
에 MonstersDetail
컴포넌트를 연결했고, 지정된 몬스터의 상세페이지를 보여주기 위해서
<Route exact path="/monsters/detail/:id" component={MonsterDetail} />
이렇게 id라는 params를 추가한 라우트를 추가해주었다.
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import "./Card.scss";
class Card extends Component {
render() {
console.log("card: ", this.props);
const { id, name, email } = this.props;
return (
<div
className="card-container"
onClick={() => this.props.history.push(`/monsters/detail/${id}`)}
>
<img src={`https://robohash.org/${id}?set=set2&size=180x180`} alt="" />
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
}
export default withRouter(Card);
리스트 페이지 안에서 Card 컴포넌트가 map 함수를 통해 렌더된다. Card 컴포넌트는 Route.js 파일에서 import 되어 사용되지 않았기 때문에 withRouter
를 import 해주고, export 에서 감싸주었다.
onClick={() => this.props.history.push(`/monsters/detail/${id}`)}
card-container
에 onClick 이벤트를 걸어서 해당 card의 props의 id 값이 담긴 url 주소로 push 해준다. 위에서 배운 라우트의 props인 history.push를 사용한 것을 확인할 수 있다. 이렇게 상세페이지로 이동하게 한다.
import React, { Component } from "react";
import Card from "./Components/Card/Card";
import "./MonsterDetail.scss";
class MonsterDetail extends Component {
state = {
data: {},
};
componentDidMount() {
fetch(
`https://jsonplaceholder.typicode.com/users/${this.props.match.params.id}`
)
.then((res) => res.json())
.then((res) => this.setState({ data: res }));
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
fetch(
`https://jsonplaceholder.typicode.com/users/${this.props.match.params.id}`
)
.then((res) => res.json())
.then((res) => this.setState({ data: res }));
}
}
render() {
const { data } = this.state;
return (
<div className="url-parameters">
<div className="btn-wrapper">
<button>Back to Monsters List</button>
</div>
<Card id={data.id} name={data.name} email={data.email} />
<div className="btn-wrapper">
<button
onClick={() =>
this.props.history.push(
`/monsters/detail/${this.props.match.params.id - 1}`
)
}
>
Previous
</button>
<button
onClick={() =>
this.props.history.push(
`/monsters/detail/${+this.props.match.params.id + 1}`
)
}
>
Next
</button>
</div>
</div>
);
}
}
export default MonsterDetail;
Detail 페이지에서는 부모 컴포넌트에서 넘어온 id 값이 담긴 상세 페이지 컴포넌트를 렌더한다.
componentDidMount() {
fetch(
`https://jsonplaceholder.typicode.com/users/${this.props.match.params.id}`
)
.then((res) => res.json())
.then((res) => this.setState({ data: res }));
}
컴포넌트가 마운트 되었을 때 해당 id의 json data를 state에 업데이트 해준다.
componentDidUpdate(prevProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
fetch(
`https://jsonplaceholder.typicode.com/users/${this.props.match.params.id}`
)
.then((res) => res.json())
.then((res) => this.setState({ data: res }));
}
}
상세페이지에서 앞 뒤의 id 값이 담긴 다른 아이템의 페이지로 이동하기 위해서 Update 함수를 추가해주었다. 현재의 match.params.id
와 이전의 match.params.id
값이 달라지면, 새로운 데이터를 fetch 하여 랜더한다.
<button
onClick={() =>
this.props.history.push(
`/monsters/detail/${this.props.match.params.id - 1}`
)
}
>
Previous
</button>
<button
onClick={() =>
this.props.history.push(
`/monsters/detail/${+this.props.match.params.id + 1}`
)
}
>
Next
</button>
상세페이지에서 previous, next 아이템으로 이동할 수 있는 버튼이다. 현재의 match.params.id
의 +1
, -1
값으로 이동하는 방식이다.
Next 버튼의 path에서 +this.props.match.params.id
에 +
를 붙인 이유는, 마이너스 계산은 자동적으로 int로 변경되는 반면 플러스는 string에 '1'
이 뒤에 붙는 형식으로 주소가 변경되기 때문이다.