프로젝트 생성
먼저 새로운 프로젝트를 생성합니다.
1 |
npx create-react-app react-hero |
router 개요
먼저 SPA 에서 왜 라우팅이 필요한지를 이해해야 합니다. 페이지가 하나 밖에 없기 때문에 하나의 페이지에서 여러가지 화면을 보여주게 되면 예를 들어, 게시판 목록보기, 게시판 상세보기, 게시물 등록하기 등을 /board 라는 유알엘에서 모두 보여주게 되면 브라우저는 유알엘을 히스토리에 저장하기 때문에 즐겨찾기, 뒤로가기시 유알엘을 히스토리에 저장해서 꺼내기 때문에 동일한 페이지의 첫번째 화면만 기억할 뿐입니다.
따라서 SPA에서 라우팅 페이지를 구성한다는 것은 페이지는 하나이지만 여러가지 화면에 각각 다른 유알엘을 매핑하는 규칙을 만든다는 것입니다. 그렇게 되어야만 브라우저가 유알엘별로 히스토리를 기억하기 때문에 뒤로가기도 가능하고 즐겨찾기도 가능하게 되는 것입니다.
redux 의 경우는 개발자 경험이기 때문에 사실상 redux를 사용하던 안하던 사용자는 알지 못합니다.. 그러나 router 의 경우는 사용자의 경험과 밀접한 관계가 있기 때문에 필수적으로 사용해야 합니다.
다행스러운것은 SPA에서의 라우팅 개념은 리액트 뿐만 아니라 앵규러, 뷰에서도 모두 동일합니다. 그러나 SPA에서의 라우팅에 대해서는 아래 개념정도는 필수적으로 알고 있어야 합니다.
- 라우팅 설정하기
- 공통 메뉴 및 링크 만들기
- 액티브 메뉴 만들기
- nested 라우팅 만들기
- 동적라우팅 개념 및 적용하기
- Not Found 메뉴 설정
- redirect 처리하기
- 코드 상에서 라우팅 처리하기
router 설정
라우팅은 리액트 기본 라이브러리에 포함되어있지 않기 때문에 3rd party 라이브러리를 설치해야 한다.
1 |
yarn add react-router-dom |
홈화면을 구성할 Home 컴포넌트, heroes 화면을 구성할 Heroes 컴포넌트, Scoreboard 화면을 구성할 Scoreboard 컴포넌트 Product 화면을 구성할 Product 컴포넌트를 각각 작성한다. 모든 컴포넌트는 펑션 컴포넌트로 만든다.
그리고, 모두 페이지 관련된 폴더인 pages 폴더를 만들고 안에 작성한다.
1 2 3 4 5 6 7 |
export const Home = (props) => { return ( <div> Home works!! </div> ) } |
1 2 3 4 5 6 7 |
export const Heroes = (props) => { return ( <div> Heroes works!! </div> ) } |
1 2 3 4 5 6 7 |
export const Scoreboard = (props) => { return ( <div> Scoreboard works!! </div> ) } |
1 2 3 4 5 6 7 |
export const Product = (props) => { return ( <div> Product works!! </div> ) } |
Root 컴포넌트를 작성하고 라우팅을 구성한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import React from 'react'; import {BrowserRouter} from "react-router-dom"; import {Heroes} from "./Heroes"; import {Home} from "./Home"; import {Scoreboard} from "./Scoreboard"; import {Product} from "./Product"; import Route from "react-router-dom/es/Route"; export const Root = (props) => { return ( <BrowserRouter> <p>공통메뉴 영역</p> <Route path="/" component={Home}></Route> <Route path="/heroes" component={Heroes}></Route> <Route path="/scoreboard" component={Scoreboard}></Route> <Route path="/product" component={Product}></Route> </BrowserRouter> ) } |
1 2 3 4 5 |
ReactDOM.render( <React.StrictMode> <Root /> </React.StrictMode>, , document.getElementById('root')); |
라우팅을 테스트해보자.
- http://localhost:3000 => Home 이 보여야 한다.
- http://localhost:3000/heroes => Home, Heroes 둘 다 보여야 한다.
라우팅은 잘되는데 왜 /heroes 로 가면 Heroes만 보이지 않고 Home과 Heroes 둘 다 보이는것일까? Route 가 만족되면 모든 Route가 보이기 때문에 그렇다. 그러므로 만족시키는 하나만 보이게 하기 위해서는 다음과 같이 Switch 문을 추가해야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export const Root = (props) => { return ( <BrowserRouter> <p>공통메뉴 영역</p> <Switch> <Route path="/" component={Home}></Route> <Route path="/heroes" component={Heroes}></Route> <Route path="/scoreboard" component={Scoreboard}></Route> <Route path="/product" component={Product}></Route> </Switch> </BrowserRouter> ) } |
/heroes로 접근하면 Home과 Heroes 둘다 보이지 않는데 Heroes가 아니라 Home이 보인다. 왜 그럴까? path는 완전히 일치 하는 경로를 보여주는게 아니라 start with 의 의미이다. path와 완전히 일치하게 할려면 exact 를 추가해야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export const Root = (props) => { return ( <BrowserRouter> <p>공통메뉴 영역</p> <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> </BrowserRouter> ) } |
공통 메뉴 만들기
상단 공통 메뉴 부분을 만들어보자. 앞서 말한것처럼 bootstrap 이라는 CSS 프레임웍을 사용해서 스캐폴딩후 메뉴바에 해당하는 nav를 이용하여 공통 메뉴바를 만들겠습니다. 먼저 bootstrap 라이브러리를 설치합니다.
bootstrap 외에 bootstrap 을 리액트 컴포넌트로 만든 라이브러리도 추가로 필요하지만 이 라이브러리가 왜 필요한지 설명하기 위해서 일단 먼저 bootstrap 만 설치합니다.
1 |
yarn add bootstrap |
bootstrap css 를 index.js에 추가한다. 순서를 index.css 앞에 둔다.
1 2 3 4 |
... import 'bootstrap/dist/css/bootstrap.css'; import './index.css'; ... |
Root 컴포넌트에 Menu 컴포넌트를 추가한다. Menu 컴포넌트에는
getbootstrap.com에 documentation > components > Navbar 에서 Nav 제목 부분 소스를 복사해서 그대로 넣는다.
webstorm이 class 부분을 알아서 className으로 변경해준다.
Navbar는 바 형태의 UI 컴포넌트이고 여기 안에 들어갈수 있는 것들의 목록이 부트스트랩 다큐먼트에 나열되어있다. 좌측 Navbar 선택후 우측 Support Content 부분을 보면 brand, nav ,form, text 4가지 컨텐트가 지원된다. 여기서는 brand와 nav 두가지를 사용하였다.
href에 위에서 설정한 라우팅 경로를 매핑한다.
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 |
export const Menu = (props) => { return ( <nav className="navbar navbar-expand-lg navbar-light bg-light"> <a className="navbar-brand" href="#">Navbar</a> <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span className="navbar-toggler-icon"></span> </button> <div className="collapse navbar-collapse" id="navbarNav"> <ul className="navbar-nav"> <li className="nav-item active"> <a className="nav-link" href="/">Home <span className="sr-only">(current)</span></a> </li> <li className="nav-item"> <a className="nav-link" href="/heroes">Heroes</a> </li> <li className="nav-item"> <a className="nav-link" href="/scoreboard">Scoreboard</a> </li> <li className="nav-item"> <a className="nav-link" href="/product">Product</a> </li> </ul> </div> </nav> ) } |
Root.jsx에는 공통 부분을 Menu.jsx로 교체한다.
1 2 3 4 5 6 7 8 9 |
<> <Menu/> <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> </> |
화면은 아래와 같이 보여야 한다.
테스트를 해보면 잘동작하는것 같다. 그러나 하나의 문제가 있다.
위에서처럼 a 태그의 href를 그대로 사용할 경우 a 태그는 기본 이벤트가 다른 페이지를 호출하므로 페이지가 재로딩되면서 깜박임 현상이 일어난다.
SPA로 개발할때는 항상 한페이지로 작업해야 하는데 a 태그나 form 태그 클릭시 페이지가 새로고침 현상이 일어나면 모든 변수가 초기화 되는 현상이 일어난다. 따라서 기본 이벤트를 막아야 한다. 기본 이벤트를 막는 부분을 react-router-dom 라이브러리의 Link, NavLink가 제공해준다. 두 개의 차이점은 NavLink는 액티브 링크일시 active 클래스를 추가해준다는 차이점이 있다. a 태그를 모두 NavLink로 바꾸고 href를 to로 변경해주면 된다.
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 |
import React from 'react'; import {NavLink} from "react-router-dom"; export const Menu = (props) => { return ( <nav className="navbar navbar-expand-lg navbar-light bg-light"> <NavLink className="navbar-brand" to="/">Navbar</NavLink> <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span className="navbar-toggler-icon"></span> </button> <div className="collapse navbar-collapse" id="navbarNav"> <ul className="navbar-nav"> <li className="nav-item active"> <NavLink className="nav-link" to="/">Home <span className="sr-only">(current)</span></NavLink> </li> <li className="nav-item"> <NavLink className="nav-link" to="/heroes">Heroes</NavLink> </li> <li className="nav-item"> <NavLink className="nav-link" to="/scoreboard">Scoreboard</NavLink> </li> <li className="nav-item"> <NavLink className="nav-link" to="/product">Product</NavLink> </li> </ul> </div> </nav> ) } |
reactstrap 라이브러리 적용
페이지 가로 사이즈를 줄여서 햄버거 메뉴가 나타나면 클릭해보자. 드랍다운 메뉴가 나와야 하는데 나오지 않는다. 왜냐하면 자바스크립트 코드를 삽입하지 않았기 때문이다.
그래서, bootstrap를 리액트 컴포넌트로 변환한 라이브러리가 더 필요하다. npmjs.com 에서 react, bootstrap 으로 검색하면 몇가지 리액트용 bootstrap 컴포넌트가 나올것이다. 선택시 다운로드 건수와, 가장 최근에 업데이트된 날짜를 확인해서 선택한다.
여기서는 다운로드 건수도 많고 최근 업데이트가 잘되고 있는 reactstrap 이라는 모듈을 사용한다. 이 모듈은 bootstrap 4.x를 지원하고, 모달, form, toast 등 개발에 필수적으로 필요한 여러가지 컴포넌트를 제공해주고 있다.
1 |
yarn add reactstrap |
reactstrap 홈페이지는 다음과 같다.
https://reactstrap.github.io/components/navbar/
오른쪽 컴포넌트 리스트에서 Navbar를 선택하고 좌측의 코드를 복사해서 먼저 똑같이 적용한다.
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 |
import React, { useState } from 'react'; import { Collapse, Navbar, NavbarToggler, NavbarBrand, Nav, NavItem, NavLink, UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem, NavbarText } from 'reactstrap'; export const Menu = (props) => { const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(!isOpen); return ( <div> <Navbar color="light" light expand="md"> <NavbarBrand href="/">reactstrap</NavbarBrand> <NavbarToggler onClick={toggle} /> <Collapse isOpen={isOpen} navbar> <Nav className="mr-auto" navbar> <NavItem> <NavLink href="/components/">Components</NavLink> </NavItem> <NavItem> <NavLink href="https://github.com/reactstrap/reactstrap">GitHub</NavLink> </NavItem> <UncontrolledDropdown nav inNavbar> <DropdownToggle nav caret> Options </DropdownToggle> <DropdownMenu right> <DropdownItem> Option 1 </DropdownItem> <DropdownItem> Option 2 </DropdownItem> <DropdownItem divider /> <DropdownItem> Reset </DropdownItem> </DropdownMenu> </UncontrolledDropdown> </Nav> <NavbarText>Simple Text</NavbarText> </Collapse> </Navbar> </div> ); } |
똑같이 적용하면 아래와 같이 보일것이다.
collapse는 잘될것이다. 그런데, 라우팅을 하면 깜빡임 현상이 다시 나타나고 또한 디버거창에 warining도 보일것이다.
메뉴에 유알엘을 위에서 한것과 같이 매핑해준다. href를 NavLink라는 컴포넌트로 교체한다.
그런데, 이렇게 교체할 경우 디자인이 깨질것이다. 위 부트스트랩에서 한 코드를 살펴보면 해당 className이 있을것이고 동일한 className을 적용하면 디자인이 복원될것이다.
이번에는 다크테마로 교체한다.
Navbar 컴포넌트에 color, light, expand 속성이 제공됨을 해당 페이지에서 확인하고 dark 테마를 적용하기 위해서 color 속성에 dark를 백그라운드에 다크 테마를 적용하기 위해서 light 대신 dark 속성을 사용한다. 속성의 값이 boolean 일 경우는 이와 같이 dark=”true” 대신 dark로 true를 생략한다.
Navbar에 container 클래스를 적용하여 가운데 배치한다.
최종적인 코드는 다음과 같다.
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 |
import React, { useState } from 'react'; import { Collapse, Navbar, NavbarToggler, Nav, NavItem } from 'reactstrap'; import {NavLink} from "react-router-dom"; export const Menu = (props) => { const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(!isOpen); return ( <Navbar color="dark" dark expand="md"> <div className="container"> <NavLink to="/" className="navbar-brand">React</NavLink> <NavbarToggler onClick={toggle} /> <Collapse isOpen={isOpen} navbar> <Nav className="ml-auto" navbar> <NavItem> <NavLink to="/heroes" className="nav-link">Heroes</NavLink> </NavItem> <NavItem> <NavLink to="/scoreboard" className="nav-link">Scoreboard</NavLink> </NavItem> <NavItem> <NavLink to="/product" className="nav-link">Product</NavLink> </NavItem> </Nav> </Collapse> </div> </Navbar> ) } |
Home 화면 진입시
/heroes로 라우팅하면 Heroes가 액티브가 되어서 더 굵게 되는것을 볼 수 있다.
모바일 화면이 되면 햄버거 메뉴가 나타나고 클릭시 메뉴가 아래로 펼쳐진다. 이 부분으 react hooks 구현이 되어서 toggle되고 있다.