리액트 라우터
[ 06 - 1 ] 처음 만나는 리액트 라우터
URL 이란 ?
리액트 라우터를 이해하려면 URL과 location.history와 같은 이름의 객체가 제공하는 속성이나 메서드를 알아야 합니다. 먼저 URL은 인터넷에서 자원의 위치를 의미합니다 여기서 자원이란 HTML, CSS, 이미지 등 웹브라우저가 이해할 수 있는 모든 형태의 데이터를 뜻합니다.
URL은 웹 브라우저가 특정 웹서버에 원하는 자원요청을 할 때 사용됩니다. 다음그림은 URL을 구성하는 요소들을 나타낸 것입니다.

URL은 먼저 'http://'나 'https://'와 같은 토신 프로토콜을 명시하고 프로토콜 다음에 호스트나 도메인 이라고 부르는 웹 서버 이름, 그리고 도메인 다음에 '/'로 시작하는 경로로 구성됩니다. 여기서 포트번호가 80이 아닐때는 도메인뒤에 : 기호를 붙이고 포트번호를 명시합니다. 그리고 ? 로 시작하는 쿼리 매개변수나 #으로 시작하는 프로그먼트 부분이 있습니다.
URL의 가장 간단한 형태는 http://localhost:3000처럼 프로토콜과 도메인만을 명시하는 것으로, 이때 웹 브라우저는 생략된 경로 / 를 추가한 http://localhost:3000을 도메인 localhost:3000에 요청하며, localhost:3000은 경로 /에 해당하는 자원을 http프로토콜을 사용하여 웹 브라우저 쪽으로 응답합니다.
location 객체 알아보기
웹 브라우저는 window.location형태로 얻을 수 있는 location 객체를 제공합니다.


history 객체 알아보기
웹브라우저는 또한 window.history 형태로 얻을수 있는 history객체도 제공합니다. location 객체가 URL을 구성하는 각종요소를 속성으로 제공한다면. history 객체는 '앞으로가기' '뒤로가기' 등 페이지 이동을 프로그래밍 메서드를 할 수 있게 제공합니다.

라우팅이란?
URLdml 'L'은 위치 제공자라는 의미입니다. 웹 서버는 많은 자원을 제공하므로 원한느 자원을 얻으려면 자원이 있는 경로와 쿼리 매개변수 등을 조합하여 웹서버로 하여금 원하는자원이 구체적으로 무엇인지 알아야합니다. 이때 웹서버에서 URL에 명시된 자원을 찾는 과정을 라우팅이라고 합니다.
보통 웹에서 라우팅은 항상 서버에서 일어납니다. 그런데 앞어 상라본 location과 history 객체의 기능으로 실제로는 라우팅이 웹브라우저 즉 클라이언트에서 일어나지만 사용자 관점에서는 서버 쪽 라우팅과 똑같은 사용자 경험을 줄 수 있습니다. 이처럼 웹브라우저에서 발생하는 라우팅을 클라이언트 측 라우팅 이라고합니다.
SPA 방식 웹 앱의 특징
웹 서버와 웹브라우저가 여러 HTML 파일을 주고받는 방식을 다중 페이지 앱줄여서 MPA라고 합니다. 반면에 라우팅이 웹 브라우저에서만 일어나는 웹 방식을 단일 페이지 앱, 줄여서 SPA라고 합니다.
SPA 방식 웹 앱은 사용자가 처음 서버에 접속할 때 다양한 컴포넌트들로 구성된 1개의 HTML파일을 웹 브라우저에 전송합니다. 그리고 이 HTML 파일에 포함된 자바스크립트 코드가 동작하면서 사용자가 원하는 내용을 DOM 구조로 만들어 가면서 보여줍니다.
SPA방식 웹 앱에서 클라이언트 측 라우팅 이하(CSR)은 수많은 컴포넌트 중 특정 컴포넌트를 찾아 화면에 보여줍니다. SPA방식 웹 앱은 자바스크립트 코드가 동작하므로 데스크톱 앱을 사용하는 것처럼 느끼게 해줍니다. 또한 CSR은 프로그래밍으로 URL을 입력하지만 실제 서버에 전송하는 URL이 아니므로 사용자가 보고 있는 컴포넌트가 바뀌어도 화면새로고침이 발생하지 않습니다.
리액트 라우터 패키지란?
리액트 라우터는 리덕스와 더불어 리액트 커뮤니티에서 가장 널리 사용되는 CSR 패키지입니다. 리액트 라우터는 2014년에 처음 만들어졌습니다. 리액트 라우터 6번전은 과전 버전과 달리 리액트 훅 기술에 기반을 두고 있습니다.
리액트 라우터 또한 컨텍스트 기반으로 실계되었승므로 다움처럼 컨텍스트 제공자인 BrowserRouter를 최상위 컴포넌트로 사용해야 합니다.
import {BrowserRouter} from 'react-router-dom'
export default function App(){
return <BrowserRouter>
/* 리액트 라우터 기능 사용 컴포넌트 */
</BrowserRouter>
}
NoMatch 컴포넌트 만들기
리액트 라우터 패키지를 사용하려면 Routes와 Route, 그리고 Link라는 이름의 컴포넌트를 알아야 합니다.
Routes, Route 컴포넌트 임포트
import { Routes, Route } from 'react-router-dom'
Route 컴포넌트는 path와 element라는 속성을 제공하며, 다음 코드는 주소창에 경로 '/'가 있을 때 Home이란 이름의 컴포넌트를 화면에 보이게 설정(라우팅)하는 예입니다.
경로 '/'를 Home 컴포넌트로 라우팅하기
import Home from './Home'
<Route path="/" element={Home />} />
만일 모든 경로를element 속성에 설정한 컴포넌트로 라우팅 하고 싶다면 다음처럼 path 경로에 *을 설정합니다.
import NoMatch from './NoMatch'
<Route path="*" element={<NoMatch />} />
그런데 Route 컴포넌트는 단독으로 사용할 수 없고 항상 Routes 컴포넌트의 자식 컴포넌트로 사용해야 합니다. 다음 src/routes 디렉터리의 RoutesSetup.tsx 파일은 모든 경로에 대해 NoMatch 컴포넌트가 화먼에 나타나도록 설정합니다. 그런데 Route는 독립적일 수 가 없으므로 Routes컴포넌트의 자식 컴포넌트로 사용되고 있습니다.


Home 컴포넌트 만들기
이번에는 경로가 / 일때 Home.tsx 파일이 화면에 나오도록 해보겠습니다.
import { FC } from "react";
type HomeProps = {
title?: string
}
const Home: FC<HomeProps> = ({title}) => {
return <p>{title ?? 'Home'}</p>
}
export default Home
그런다음 RoutesSetup.tsx에서 Home컴포넌트의 라우트 경로를 설정합니다. 이 코드는 왜 Route 컴포넌트가 component라는 이름이 아닌 element라는 이름의 속성을 제공하는지 힌트를 줍니다. 즉. 만일 component={Home}처럼 사용하도록 설계했다면 라우트 경로에 맞는 컴포넌트가 제공하는 속성을 사용할 수 없지만 , element는 element={<Home title="hello" />} 형태로 컴포넌트속성에 적절한 값을 설정 할 수 있습니다.


Link 컴포넌트 알아보기
HTML에서 다른 웹 사이트로 이동하거나 특정 경로의 내용을 보려면 <a href="다른_사이트_url"> 요소를 사용합니다. 다만 <a> 요소는 클라이언트 측 라우팅을 위한 용도로는 바로 사용 할 수 없습니다. 이 때문에 리액트 라우터는 <a> 요소를 감싼 Link 컴포넌트를 다음처럼 제공합니다.
Link 컴포넌트 임포트
import {Link} from 'react-router-dom'
Link 컴포넌트는 <a> 요소의 href 속성 대신 다음처럼 to 속성을 제공합니다.
<Link to="/Home">Home</Link>
이제 앞선 코드와 다르게 NoMatch대신 Board가 컴포넌트 화면에 출력되게 하겠습니다.


useNavigate 훅 알아보기
import { useNavigate } from 'react-router-dom'
useNavigate 훅을 호출하면 다음 코드에서 보듯 navigate함수를 얻을 수 있습니다.
const navigate = useNavigate()
navigate 함수는 다음처럼 매개변수로 전달한 경로로 이동하게 해줍니다.
navigate('/')
navigate 함수는 window.history객체의 go 메서듶처럼 -1과같은 숫자로 뒤로가기 등의 효과도 줄수 있습니다.
navigate(-1)


위에 사진은 라우터 설정에없는 경로로 가서 NoMatch페이지가 나오고 go back버튼을 통하여 -1 즉 이전페이지로 돌아가게 할수있는 화면입니다.
라우터 변수란?
라우트를 설정할 때 라우트 경로는 다음처럼 콜론(:)을 붙일 수 있는데 이처럼 콜론을 앞에 붙인 uuid, title과 같은 심볼을 라우트 변수 라고 합니다. 경로의 일정부분이 수시로 바뀔때 사용합니다.
라우트 변수
<Route path="/board/card/:cardid" element={<Card />} />


위 사진을보면 첫번째 사진의 경로가 uuid로 나온걸 알 수 있습니다 그리고 'board/card/:cardid' 경로에 대한 화면 컴포넌트가 없으므로 noMatch 컴포넌트가 나타났습니다.
useLocation 훅 알아보기
import { useLocation } from 'react-router-dom'
useLocation 훅은 다음처럼 location 객체를 얻습니다.
const location = useLocation()
locatcion 객체는 웹 브라우저가 기본으로 제공하는 window.location과 개념적으로 비슷하나 완전히 똑같지는 않습니다. 이제 location 객체가 어떤 정보를 담고 있는지 Card.tsx파일에 구현해 보겠습니다.


위 사진의 실행결과로 통해 location.pathname으로 현재 실행된 라우터 경로를 얻을수 있음을 알수 있습니다.
useParams 훅 알아보기
리액트 라우터는 다음처럼 useParams 훅을 제공합니다.
import { useParams } from 'react-router-dom'
useParams 훅은 다음처럼 params 객체를 얻습니다. 그리고 이 params 객체로부터 라우트 매개변숫값을 얻습니다.
const params = useParams()
const params = useParams()
<p>params: {JSON.stringify(params, null, 2)}</p>
params는 '라우트_매개변수_이름: 값' 형태의 정보를 가지는 Record<string, any> 타입의 객체임을 알 수 있습니다.
useSearchParams 훅 알아보기
리액트 라우터는 다음처럼 useSearchParams 훅도 제공합니다.
import {useSearchParams } from 'react-router-dom'
useSearchParams 훅은 다음처럼 searchParams 객체와 setSearchParams세터함수를 튜플 형태로 반환합니다.
const [searchParams, setSearchParams] = useSearchParams()
라우트 경로에 쿼리 매개변수가 '?from=0&&to=20'처럼 포함되었을 때, from과 to 쿼리 매개변수는 다음처럼 searchParams의 get 메서드로 얻을 수 있습니다.그런데 이런구조는 코드를 불필요하게 복잡하게 만드므로 useQueryString이란 커스텀 훅을 만들겠습니다

위사진은 카드를 클릭하여 쿼리 매개변수가 없는 라우트 경로를 만든 다음, 주소창에서 쿼리 매개변수 부분(?from=10&to=50)만 직접 작성하여 최종 라우트 경로를 만든 것 입니다.
카드 상세 페이지 만들기
import type { AppState } from "../store"
import type {Card as CardType} from '../store/commonTypes'
import * as CE from '../store/cardEntities'
import { useCallback, useState, useEffect } from "react"
import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"
import { UseSelector } from "react-redux"
import { Div, Avatar } from "../components"
import { useSelector } from "react-redux"
export default function Card() {
const location = useLocation()
const params = useParams()
const navigate = useNavigate()
const [search] = useSearchParams()
const goback = useCallback(() => {
navigate(-1)
}, [navigate])
const [card, setCard] = useState<CardType | null>(null)
const {cardid} = params
const cardEntities = useSelector<AppState, CE.State>(({cardEntities}) => cardEntities)
useEffect(() => {
if (!cardEntities || !cardid) return
cardEntities[cardid] && setCard(notUsed => cardEntities[cardid])
}, [cardEntities, cardid])
if(!card){
return (
<div>
<p>location: {JSON.stringify(location, null, 2)}</p>
<p>params: {JSON.stringify(params, null, 2)}</p>
<p>cardid: {params.cardid}</p>
<p>from: {search.get('from')} to: {search.get('to')}</p>
<button className="mt-4 btn btn-primary btn-xs" onClick={goback}>
뒤로가기
</button>
</div>
)
}
return(
<div className="p-4">
<Div src={card.image} className="w-full" minHeight="10rem" height="10rem" />
<Div className="flex flex-row items-center mt-4">
<Avatar src={card.writer.avatar} size="2rem" />
<Div className="ml-2">
<p className="text-xs font-bold">{card.writer.name}</p>
<p className="text-xs text-gray-500">{card.writer.jobTitle}</p>
</Div>
</Div>
<button className="mt-4 btn btn-primary btn-xs" onClick={goback}>
go Back
</button>
</div>
)
}

