요약
+ 동적 라우팅
+ 네스티드 라우팅 + 동적 라우팅 결합 형태
상세보기 페이지 구성하기
상세보기 페이지를 구성시 별도의 페이지로 구성할수도 있고, 목록안에 포함해서 구성할수도 있다.
쉽게 얘기하자면 게시판 목록에서 게시판 목록을 클릭하면 상세화면이 나오는데, 목록이 사라지고 상세화면이 나오게 할 수도 있고, 하단에 목록은 그대로 있고 상단에 상세화면을 보여주는 방법도 있다. 물론 두번째 방법이 사용자 입장에서는 더 편리할 수 있다.
첫번째는 동적 라우팅이라고 할수 있고, 두번째는 네스티드 라우팅 + 동적 라우팅이 결합된 형태이다. 어떻게 구성을 하던 동적라우팅이 포함되어야 하고 동적 라우팅이 구성되어야 하는 이뉴는 별도의 유알엘이 구성되어야 브라우저가 히스토리에 기억하고 뒤로가기, 즐겨찾기 등을 할 수가 있기 때문이다.
먼저 첫번째 방법으로 동적 라우팅을 구성한다음 두번째 방법으로 확장을 해보자.
동적 url 라우팅 설정
동적 url이란 유알엘이 고정된게 아니라 변한다는 의미이다.
기존에는 패스가 고정되었지만 디테일 페이지는 동적으로 변경되는 아이디를 URI 파라메터 혹은 Query 파라메터로 받는 동적 라우팅을 설정해야 한다.
먼저 Hero 컴포넌트를 생성한다. pages 폴더 아래에 hero 폴더를 만들고 생성한다.
1 2 3 4 5 6 7 |
export const Hero = (props) => { return ( <div> Hero works!! </div> ) } |
동적 라우팅 패스를 설정한다. 동적 라우팅을 설정하는 방법은 콜론 다음에 동적으로 변하는 파라메터를 설정한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
export const Root = (props) => { return ( <BrowserRouter> <Menu></Menu> <div className="container" style={{backgroundColor: '#ffffff'}}> <Switch> <Route exact path="/" component={Home}></Route> <Route path="/heroes" component={Heroes}></Route> <Route path="/hero/:id" component={Hero}></Route> <Route path="/scoreboard" component={Scoreboard}></Route> <Route path="/product" component={Product}></Route> </Switch> </div> </BrowserRouter> ) } |
브라우저에 /hero/1 이라고 쳐보자 Hero works가 나타나면 제대로 설정이 된것이다.
uri parameter 얻기
/hero/1 과 같은 형태를 uri parameter라고 한다. 만일 query parameter라면 /hero?id=1 과 같은 형태가 된다. 둘다 브라우저에서 히스토리를 따로 기록하기 때문에 차이가 없다. 다만 uri parameter가 좀 더 user 친화적이다. 이제 이 뒤에 동적으로 변하는 파라메터를 추출해보자.
콘솔로 출력해보면 props 객체에 match 의 params 객체에 id 파라메터가 포함된것을 알수 있다.
1 2 3 4 5 6 7 8 9 |
export const Hero = (props) => { console.log('View: ', props); return ( <div> Hero works!! </div> ) } |
서버 연동 하기
이 파라메터를 추출 후에 REST api를 호출해보자 상세 페이지를 호출하는 REST api는 다음과 같다.
- protocol: /api/user/hero/:id
- method: GET
- uri parameter: id
- example) /api/user/hero/1
axios로 데이터를 가져와보자. state에 hero 객체를 정의했다. 처음 실행때는 값이 없다가 네트워크를 통해서 값을 가져오게 되므로 시간애 따라 변경이 일어나는 데이터이므로 state로 관리해야 한다.
useState로 설정하고 초기값을 null로 설정했다.
DOM이 렌더링 된 직후에 데이터를 최초 한번 가져와야 하므로 useEffect 두번째 파라메터에 빈 배열을 넘겼다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import React, {useEffect, useState} from 'react'; import api from "../../utils/api"; export const Hero = (props) => { const [hero, setHero] = useState(null); useEffect(() => { getHero(props.match.params.id); }, []); const getHero = async (id) => { let response = await api.get(`/api/user/hero/${id}`); console.log(response); if (response.data) { setHero(response.data); } } return ( <div> Hero works!! </div> ) } |
hero 라는 모델 데이터값을 얻어왔으므로 bootstrap table 클래스를 사용하여 View를 만들고 바인딩을 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
... return ( hero ? <div> <table className="table"> <tbody> <tr> <th scope="row">Name</th> <td>{hero.name}</td> </tr> <tr> <th scope="row">Email</th> <td>{hero.email}</td> </tr> <tr> <th scope="row">Sex</th> <td>{hero.sex}</td> </tr> <tr> <th scope="row">Country</th> <td>{hero.country}</td> </tr> <tr> <th scope="row">Power</th> <td>{hero.powers.toString()}</td> </tr> <tr> <th scope="row">Photo</th> <td> { hero.photo ? <img src={hero.photo} alt={hero.name} style={{maxWidth: '100%'}}></img> : '' } </td> </tr> </tbody> </table> <hr className="my-5" /> </div> : '' ) |
아래와 같은 화면이 나오는지 확인하자.
네스티드 라우팅 + 동적 라우팅 결합형태
목록을 표시하고 목록을 클릭하면 목록위에 (혹은 아래) 상세 페이지를 보여주는 네스팅 구조의 라우팅을 만들어보자. 이런 네스팅 구조가 SPA의 가장 큰 특징이다.
Root.js에 추가한 /hero/:id 라우팅은 삭제한다.
Heroes.js에 네스티드 라우팅 패스를 추가한다.
네스티드 라우팅은 전체 경로가 반드시 부모유알엘 + 자식유알엘 형태로 되어야 부모가 자식을 포함할수 있다.
여기서 부모 유알엘은 /heroes 이고 자식 유알엘은 /hero/:id 라는 동적 유알엘을 포함하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
... return ( <> <Switch> <Route path="/heroes/hero/:id" component={Hero}></Route> </Switch> <div className="row"> {heroes.map(hero => ( <div className="col-12 p-1 col-sm-4 p-sm-2 col-md-3 p-md-3" key={hero.id}> <div className="card"> <img src={hero.photo ? hero.photo : process.env.PUBLIC_URL + '/images/face-black-18dp.svg'} style={{width: '100%'}} alt={hero.name}></img> <div className="card-body"> <h5 className="card-title">{hero.name}</h5> <p className="card-text">email: {hero.email}</p> </div> </div> </div> ))} </div> <Pagination total={totalCount} current={currentPage} pageSize={pageSize} onChange={(page) => setCurrentPage(page)} className="d-flex justify-content-center" /> </> ) ... |
Hero.jsx에는 아래쪽에 hr 구분자를 한줄 추가한다. m은 마진이고 y는 top과 bottom 에 모두 적용하라는 의미이다.
1 2 |
... <hr classname="my-5"> |
이제 /heroes/hero/1 이런식으로 테스트를 해보자.
목록에서 Link 연결
목록의 카드를 누르면 상세페이지로 라우팅되도록 연결하자. 현재 Card로 된 UI는 div로 감싸져있다. 그런데 router는 Link or NavLink라는 컴포넌트만 제공된다. angular 처럼 directive가 제공되지 않으므로 click 이벤트시 props.history.push() 를 사용해서 이동시켜야 한다. 주의할 점은 hyperlink에 있는 기본이벤트를 막아야 깜박이며 페이지가 리로딩되는 현상을 막을수 있다.
1 2 3 4 5 6 7 8 |
... <div className="card" key={hero.id} onClick={(e) => handleClick(e, hero.id)} style={{cursor: 'pointer'}}> ... const handleClick = (event, id) => { console.log(event, id); event.preventDefault(); props.history.push(`/heroes/hero/${id}`); } |
목록보기에서 첫번째 클릭하면 제대로 자식 컴포넌트가 보이는데, 두번째 컴포넌트를 클릭하면 변화가 없다. componentDidMount 역할을 하도록 useEffect 두번째 파라메터에 빈배열을 넘겼기 때문이다. 이는 컴포넌트가 생성될때 한번만 불리기 때문이다. 그래서 useEffect 두번째 파라메터로 props.match.params.id 값을 설정한다.
1 2 3 4 5 |
... useEffect(() => { getHero(props.match.params.id); }, [props.match.params.id]); ... |
완성된 Hero.jsx는 아래와 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import React, {useEffect, useState} from 'react'; import api from "../../utils/api"; export const Hero = (props) => { const [hero, setHero] = useState(null); useEffect(() => { getHero(props.match.params.id); }, [props.match.params.id]); const getHero = async (id) => { let response = await api.get(`/api/user/hero/${id}`); console.log(response); if (response.data) { setHero(response.data); } } return ( hero ? <div> <table className="table"> <tbody> <tr> <th scope="row">Name</th> <td>{hero.name}</td> </tr> <tr> <th scope="row">Email</th> <td>{hero.email}</td> </tr> <tr> <th scope="row">Sex</th> <td>{hero.sex}</td> </tr> <tr> <th scope="row">Country</th> <td>{hero.country}</td> </tr> <tr> <th scope="row">Power</th> <td>{hero.powers.toString()}</td> </tr> <tr> <th scope="row">Photo</th> <td> { hero.photo ? <img src={hero.photo} alt={hero.name} style={{maxWidth: '100%'}}></img> : '' } </td> </tr> </tbody> </table> <hr className="my-5" /> </div> : '' ) } |
완성된 Heroes.jsx는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
import React, {useEffect, useState} from 'react'; import api from "../../utils/api"; import Pagination from 'rc-pagination'; import {Switch, Route} from "react-router-dom"; import {Hero} from "../hero/Hero"; export const Heroes = (props) => { const [heroes, setHeroes] = useState([]); const [pageSize, setPageSize] = useState(10); const [totalCount, setTotalCount] = useState(0); const [currentPage, setCurrentPage] = useState(1); useEffect(() => { getHeroes() }, [currentPage]); const getHeroes = async () => { let response = await api.get(`/api/user/heroes?start_index=${pageSize * (currentPage - 1)}&page_size=${pageSize}`); console.log(response); setHeroes(response.data.data); setTotalCount(response.data.total); } const handleClick = (event, id) => { console.log(event, id); event.preventDefault(); props.history.push(`/heroes/hero/${id}`); } return ( <> <Switch> <Route path="/heroes/hero/:id" component={Hero}></Route> </Switch> <div className="row"> {heroes.map(hero => ( <div className="col-12 p-1 col-sm-4 p-sm-2 col-md-3 p-md-3" key={hero.id}> <div className="card" key={hero.id} onClick={(e) => handleClick(e, hero.id)} style={{cursor: 'pointer'}}> <img src={hero.photo ? hero.photo : process.env.PUBLIC_URL + '/images/face-black-18dp.svg'} style={{width: '100%'}} alt={hero.name}></img> <div className="card-body"> <h5 className="card-title">{hero.name}</h5> <p className="card-text">email: {hero.email}</p> </div> </div> </div> ))} </div> <Pagination total={totalCount} current={currentPage} pageSize={pageSize} onChange={(page) => setCurrentPage(page)} className="d-flex justify-content-center" /> </> ) } |
완성된 Root.jsx는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import React from 'react'; import {BrowserRouter, Route, Switch} from "react-router-dom"; import {Heroes} from "./heroes/Heroes"; import {Home} from "./Home"; import {Scoreboard} from "./Scoreboard"; import {Product} from "./Product"; import {Menu} from "./Menu"; export const Root = (props) => { return ( <BrowserRouter> <Menu></Menu> <div className="container" style={{backgroundColor: '#ffffff'}}> <Switch> <Route exact path="/" component={Home}></Route> <Route path="/heroes" component={Heroes}></Route> <Route path="/scoreboard" component={Scoreboard}></Route> <Route path="/product" component={Product}></Route> </Switch> </div> </BrowserRouter> ) } |