FrontEnd/React

[React] 처음 만나는 리액트 라우터

일락。 2024. 12. 27. 14:06

URL이란?

 URL(uniform resource locator)은 인터넷에서 자원의 위치를 의미한다.  여기서 자원이란 HTML, CSS 이미지 등 웹 브라우저가 이해할 수 있는 모든 형태의 데이터를 뜻한다.

[출처] developer.mozilla.org

 

 

 location 객체 알아보기

  웹 브라우저에서 URL은 주소 창(address bar)에 주소를 입력하며 웹 브라우저는 그걸 통해 window.location 형태로 얻을 수 있는 location 객체를 제공한다.

 

location 객체의 속성 의미
 href  주소 창에 입력된 URL 전체 문자열을 얻거나 다른 URL로 설정하고 싶을 때 사용
 protocol  http, https같은 프로토콜 문자열을 얻고 싶을때 사용되며 콜론(:)도 포함
 host  도메인과 포트 번호가 결합된 문자열을 얻는데 사용
 pathname  도메인 '/' 문자 뒤 경로 부분을 문자열로 얻는데 사용
 search  쿼리 매개변수(parameters) 문자열을 얻고 싶을 때 사용
 hash  프래그먼트(Anchor)를 얻고 싶을 때 사용

 

 

 웹 브라우저가 제공하는 history 객체 알아보기

  location이 객체가 URL을 구성하는 각종 요소를 제공했다면 history 객체는 '앞으로 가기', '뒤로 가기' 등 페이지 이동을 지원한다.

 

history 객체의 메서드 의미
back() 뒤로 가기를 메서드를 통해 수행
forward() 앞으로 가기를 메서드를 통해 수행
go(숫자 혹은 url) go(-1)은 뒤로가기, go(1)은 앞으로가기, go(-2)는 뒤로 두번가기

 

 

 라우팅

  URL의 'L'은 위치 제공자(locator)를 의미한다.  이렇듯 웹 서버에서 URL에 명시된 자원을 찾는 과정을 라우팅(routing)이라고 한다.  보통 웹에서 라우팅은 항상 서버에서(server-side routing) 일어난다.  

 

  다만 앞서 알아본 location, history 객체의 기능으로 실제 라우팅이 웹 브라우저, 클라이언트에서 일어나지만 사용자 관점에서는 서버 쪽 라우팅과 똑같은 경험을 줄 수 있으며 이를 클라이언트 측 라우팅(slient-side routing)이라고 한다.

 

 

 SPA 방식 웹 앱의 특징

  웹 서버와 웹 브라우저가 여러 HTML 파일을 주고받는 방식을 다중 페이지 앱(multi page application) MPA라고 한다.  반면 라우팅이 웹 브라우저에서만 일어나는 웹 방식을 단일 페이지 앱(single page application) SPA라고 한다.

 

  SPA는 프로그래밍으로 URL을 입력해 실제 서버에 전송되는 URL이 아니기 때문에 컴포넌트가 바뀌어도 화면이 새로고침(refresh)가 발생하지 않는다.

 

 

리액트 라우터 패키지란?

  리액트 라우터는 사용자가 요청한 URL에 따라 알맞는 자원을 제공하는 기능을 뜻한다.  리액트 라우터 패키지는 리액트 훅 기술에 기반을 두고 있다.  그리고 컨텍스트 기반으로 설계되었기에 컨텍스트 제공자인 BrowserRouter를 최상위 컴포넌트로 사용해야 한다.

import {BrowserRouter} from 'react-router-dom'

export default function App() {
    retrun <BrowserRouter>
    /* 리액트 라운터 기능 사용 컴포넌트 */
    </BrowserRouter>
}

 

 라우터 패키지는 npm을 통해 설치 되어야 한다.  설치 후 routes 폴더를 생성해 라우터를 관리한다.  안에 포함될 파일들은 

npm i react-router-dom

 

 

 마지막으로 앱파일(src/App.tsx)에 BrowserRouter 컨텍스트를 추가한다.

import {Provider as ReduxProvider} from 'react-redux'
import {DndProvider} from 'react-dnd'
import {HTML5Backend} from 'react-dnd-html5-backend'
import {BrowserRouter} from 'react-router-dom'
import RoutesSetup from './routes/routesSetup'
import {useStore} from './store'
import Board from './pages/Board'

export default function App() {
  const store = useStore()
  return (
    <ReduxProvider store={store}>
      <DndProvider backend={HTML5Backend}>
        <BrowserRouter>
          <RoutesSetup />
        </BrowserRouter>
      </DndProvider>
    </ReduxProvider>
  )
}

 

 

 

NoMatch 컴포넌트 만들기

  페이지를 찾을 수 없을 때 사용될 NoMatch를 routes에 추가한다.

export default function NoMatch() {
  return <p className="p-4 text-xl text-center alert alert-error">Oops! No page found!</p>
}

 

 리액트 라우터 패키지를 사용하려면 Routes와 Route, 그리고 Link라는 이름의 컴포넌트를 이해해야 한다. 

 Route 컴포넌트는 path와 element 속성을 제공한다.  Route컴포넌트는 독립적으로 사용할 수 없으며 Routes 컴포넌트의 자식 컴포넌트로 사용해야 한다.

import Home from './Home'
import NoMatch from './NoMatch'
<Routes>
    <Route path="/" element={<Home />} />
    <Route path="*" element={<NoMatch />} />
</Routes>

 

 

 여기서는 routes RouteSetup을 구성해 라우팅 경로를 설정하도록 하자.  현재는 모든 경로에서 <NoMatch />  컴포넌트가 보이도록 설정한다.

import {Routes, Route} from 'react-router-dom'
import NoMatch from './NoMatch'
export default function routesSetup() {
  return (
    <Routes>
      <Route path="*" element={<NoMatch />} />
    </Routes>
  )
}

 

실행결과로 NoMatch 컴포넌트가 모든 경로에 표시되도록 설정된 것을 확인할 수 있다.

 

 

 

Home 컴포넌트 만들기

 Home 컴포넌트는 NoMatch 컴포넌트와 다르게 title이라는 선택 속성을 추가한다.  이를통해 route의 element속성을 통해 컴포넌트의 속성에 적절한 값을 설정해 줄 수 있다.

import type {FC} from 'react'
import { Link } from 'react-router-dom'

type HomeProps = { title?: string }

const Home: FC<HomeProps> = ({title}) => { return (<p>{title ?? 'Home'}</p>) }
export default Home

 

 

RoutesSetup의 라우팅을 변경한다.  아래와 같이 /welcome의 경우 title 속성값을 전달해주고 있는 것을 알 수 있다.

import {Routes, Route} from 'react-router-dom'
import NoMatch from './NoMatch'
import Home from './Home'
export default function routesSetup() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/welcome" element={<Home title="welcome to our site" />} />
      <Route path="*" element={<NoMatch />} />
    </Routes>
  )
}

 

실행결과

 

 

 

 

Link 컴포넌트 알아보기

 HTML에서는 <a href="다른_사이트url"> 요소를 사용한다.  다만 <a> 요소는 클라이언트 측 라우팅을 위한 용도로는 바로 사용할 수 없기에 리액트 라우터는 <a> 요소를 감싼 Link 컴포넌트를 제공한다.

import {Link} from 'react-router-dom'
<Link to="/">Home</Link>   // href 대신 to를 제공

 

 Board에 Link컴포넌트를 추가해 좀더 다이나믹한 화면으로 변경하도록 하겠다.  다만 Board의 경우 네비게이션바가 사라지는 부분에 대해서는 추후 처리할 예정이다.

import type {FC} from 'react'
import {Link} from 'react-router-dom'

type HomeProps = {
  title?: string
}

const Home: FC<HomeProps> = ({title}) => {
  return (
    <div>
      <div className="flex p-4 bg-gray-200">
        <Link to="/">Home</Link>
        <Link to="/welcome" className="ml-4">
          Welcome
        </Link>
        <Link to="/board" className="ml-4">
          Board
        </Link>
      </div>
      <p className="text-xl text-center text-bold">{title ?? 'Home'}</p>
    </div>
  )
}

export default Home

 

 RouteSetup에 route를 추가한다.

<Route path="/board" element={<Board />} />

 

실행결과

 

 

useNavigate 훅 알아보기

 useNavigate 훅은 호출시 navigate 함수를 얻어 매개변수에 값을 전달함으로서 전달한 경로로 이동하게 해준다. 

import {useNavigate} from 'react-router-dom'
const navigate = useNavigate()
navigate('/')  // '/' 경로로 이동
navigate(-1)   // window.history 객체의 go 메서드처럼 뒤로가기 등의 효과가능

 

 

 useNavigate 훅이 제공하는 기능을 NoMatch 컴포넌트에 추가하자.

import {useCallback} from 'react'
import {useNavigate} from 'react-router-dom'

export default function NoMatch() {
  const navigate = useNavigate()

  const goBack = useCallback(() => {
    navigate(-1)
  }, [navigate])
  return (
    <div className="flex flex-col p-4">
      <p className="p-4 text-xl text-center alert alert-error">Oops! No page found!</p>
      <div className="flex justify-center mt-4">
        <button className="ml-4 btn btn-primary btn-xs" onClick={goBack}>
          go Back
        </button>
      </div>
    </div>
  )
}

 

실행결과 "GO BACK"버튼을 눌러 전 페이지로 이동

 

 

 

라우트 변수란?

 라우트를 설정 시 라우트 경로에 콜론(:)을 붙일 경우 콜론 앞에 붙인 uuid, title같은 심볼을 라우트 변수(route parameter)라고 한다.  라우트 변수는 라우트 경로의 일정 부분이 수시로 바뀔 때 사용된다.

<Route path="/board/card/:cardid" element={<Card />} />

 

이 url을 설정하기 위해 BoardList의 소스에 일부분을 추가한다.

...(생략)...
import {useMemo, useCallback} from 'react'   // useCallback훅 추가
import {useNavigate} from 'react-router-dom' //  useNavigate훅 추가
...(생략)...

export type BoardListProps = {
  list: List
  onRemoveList?: () => void
  index: number
  onMoveList: MoveFunc
}
const BoardList: FC<BoardListProps> = ({
  list,
  onRemoveList,
  index,
  onMoveList,
  ...props
}) => {
  const {cards, onPrependCard, onAppendCard, onRemoveCard} = useCards(list.uuid)

  const navigate = useNavigate()    // 추가
  const cardClicked = useCallback(  // 추가
    (cardid: string) => () => {
      navigate(`/board/card/${cardid}`)
    },
    [navigate]
  )

  const children = useMemo(
    () =>
      cards.map((card, index) => (
        <ListCard
          key={card.uuid}
          card={card}
          onRemove={onRemoveCard(card.uuid)}
          draggableId={card.uuid}
          index={index}
          onClick={cardClicked(card.uuid)} // 추가
        />
      )),
    [cards, onRemoveCard]
  )
  return (
   ...(생략)...
  )
}

export default BoardList

Board에서 카드를 클릭시 '/board/card/:cardid' 형태로 url이 생기도록 설정

 

 

 

Card 컴포넌트 만들기

 라우트 변수를 설정한 Route를 RoutesSetup에 추가한다.

<Route path="/board/card/:cardid" element={<Card />} />

 

 

Card 컴포넌트에 리액트 라우트 훅 적용하기

  리액트 라우터가 제공하는 몇가지 훅을 통해 Card 컴포넌트를 구현하자.

 

 useLocation 훅 알아보기

  리액트라우터는 useLocation훅을 제공하며 location객체를 얻는다.  location객체는 window.location과 개념적으로는 비슷하나 같진 않다.  Card 컴포넌트에 useLocation 훅을 포함해 추가한다.

import {useCallback} from 'react'
import {useLocation, useNavigate} from 'react-router-dom'

export default function Card() {
  const location = useLocation()

  const navigate = useNavigate()
  const goBack = useCallback(() => {
    navigate(-1)
  }, [navigate])
  return (
    <div>
      <p>location : {JSON.stringify(location, null, 2)}</p>
      <button className="mt-4 btn btn-primary btn-xs" onClick={goBack}>
        go Back
      </button>
    </div>
  )
}

 

실행결과로 카드 컴포넌트에서 location을 통한 결과값을 확인할 수 있다.

 

 

 useParams 훅 알아보기

  리액트 라우터는 useParams 훅을 제공하며 params 객체를 얻는다.  이 객체는 라우트 매개변수 값을 얻어올 수 있으며 params는 '라우트_매개변수_이름: 값' 형태의 정보를 가지는 Record<string, any> 타입의 객체이다.

import {useParams} from 'react-router-dom'
const params = useParams()
<p>params: {JSON.stringify(params, null  2)}</p>

// 실행결과
params: {"cardid" : "d129292-dkwj-12i2kdkdj"}

 

 

  useSearchParams 훅 알아보기

   리액트 라우터는 useSearchParams 훅을 제공하며 searchParams 객체와 setSearchParams 세터함수를 튜플 형태로 반환한다.

const [searchParams, setSearchParams] = usesearchParams() // setSearchParams는 잘 사용안함

 

  라우트 경로의 쿼리 매개변수가 '?from=0&&to=20'처럼 포함되어 있다면 from, to 매개변수는 searchParams의 get 메서드로 얻어올 수 있다.  다만 이런 구조는 코드를 복잡하게 하므로 useQueryString 커스텀 훅을 만들어 처리하자.

const from =searchParams.get('from')
const to = searchParams.get('to')

 

 

 Card 컴포넌트에 리액트 라우터 훅 적용하기

  Card컴포넌트에 uselocation, useParams, useSearchParams 훅을 좀더 간단하게 사용하고자 useQueryString을 사용해 코드를 수정하자.  마지막 url에는 from, to의 값을 요청값으로 전달한다.

import {useCallback} from 'react'
import {useLocation, useNavigate, useParams, useSearchParams} from 'react-router-dom'

export default function Card() {
  const location = useLocation()
  const params = useParams()
  const navigate = useNavigate()
  const [search] = useSearchParams()
  const goBack = useCallback(() => {
    navigate(-1)
  }, [navigate])
  return (
    <div className="p-4">
      <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}>
        go Back
      </button>
    </div>
  )
}

 

실행결과로 from&to는 url에 추가적으로 입력해 전달하였다.

 

 

 

카드 상세 페이지 만들기

  현재 Card컴포넌트에서는 파라미터만 표현해주고 있기 때문에 상세 페이지를 추가하자.

import {AppState} from '../store'
import {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'

export default function Card() {
  const location = useLocation()
  const params = useParams()
  const navigate = useNavigate()
  const [search] = useSearchParams()
  const goBack = useCallback(() => {
    navigate(-1)
  }, [navigate])
  
  // useState를 사용해 cardid가 없거나 있더라도 cardEntities[cardid]를 통해
  // 얻는값이 null일 수 있기때문에 방어적인 코드 처리
  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] null 확인
    cardEntities[cardid] && setCard(notUsed => cardEntities[cardid])
  }, [cardEntities, cardid])
  
  if (!card) {
    return (
      ...(기존소스생략)....
    )
  }
  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>
  )
}

 

실행결과로 상세 페이지가 구성된 것을 확인할 수 있다.