본문 바로가기

FrontEnd/React

[React] Outlet 컴포넌트와 중첩 라우팅

리액트 라우터의 Outlet 컴포넌트

 리액트 라우터의 Outlet 컴포넌트는 라우트를 중첩하여 내비게이션 메뉴나 바닥글처럼 컴포넌트마다 공통으로 사용하는 부분의 코드를 줄여준다.  Outlet 컴포넌트는 다른 컴포넌트들이 렌더링되는 위치를 지정해 주는 역할을 한다.

 

 Outlet 컴포넌트를 Layout에 추가한다.  여기서 Outlet 컴포넌트는 RoutesSetup에서 설정한 라우트 경로의 컴포넌트가 여기에 위치하는 역할을 한다.

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

export default function Layout() {
  return <Outlet />
}

 

 그리고 RoutesSetup에 LayOut을 추가해 Outlet 컴포넌트에 다른 컴포넌트가 위치하게 하기 위해 중첩 라우트를 통해 설정한다.  즉 Board, NoMatch는 Layout의 자식으로 설정된다.

 

 리액트에서 부모 컴포넌트는 자식 컴포넌트 안에서 렌더링 될 수 없고, 형제 컴포넌트가 다른 형제 컴포넌트 안에서 렌더링 될 수 없다.  따라서 NoMatch가 Outlet안에서 렌더링되려면 라우터 설정은 부모/자식 관계의 라우팅, 중첩 라우팅 형태로 설정해야 한다.

import {Routes, Route} from 'react-router-dom'
import NoMatch from './NoMatch'
import Board from '../pages/Board'
import Layout from './Layout'
export default function routesSetup() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>  // 중첩라우트
        <Route path="/board" element={<Board />} />
        <Route path="*" element={<NoMatch />} />
      </Route>
    </Routes>
  )
}

 

 

 색인 라우트 알아보기

  Route 컴포넌트는 index란 이름의 boolean 타입의 속성을 제공하는데 <Route index /> 형태로 사용하는 Route를 색인 라우트(index route)라고 한다.

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

 

실행결과로 색인 라우트로 설정된 LandingPage가 표시되는 것을 확인할 수 있다.

 

 

 

네비게이션 메뉴와 바닥글 만들기

 outlet과 중첩라우트를 통해 네비게이션 메뉴와 바닥글을 공통적으로 사용할 수 있도록 구성해보자.  우선 NavigationBar와 Footer 코드를 추가한다.

// navigationBar
import {Link} from 'react-router-dom'

export default function NavigationBar() {
  return (
    <div className="flex p-2 navbar bg-base-100">
      <Link to="/">Home</Link>
      <Link to="/board" className="ml-4">Board</Link>
    </div>
  )
}

// footer
import * as D from '../../data'

export default function Footer() {
  return (
    <footer className="p-4 footer footer-center bg-primary text-primary-content">
      <div>
        <p>Copyright @ 2022 - All right reserved by {D.randomCompanyName()}</p>
      </div>
    </footer>
  )
}

 

 마지막으로 outlet을 설정한 Layout 컴포넌트에 NavigationBar와 Footer를 추가한다.

import {Outlet} from 'react-router-dom'
import NavigationBar from './NavigationBar'
import Footer from './Footer'

export default function Layout() {
  return (
    <>
      <NavigationBar />
      <Outlet />
      <Footer />
    </>
  )
}

 

실행결과로 네비게이션바와 푸터를 갖는 레이아웃 설계를 확인할 수 있다.

 

 

랜딩 페이지 만들기

 사용자가 특정 웹 사이트를 접속 시 처음 보게 되는 페이지를 랜딩 페이지(landing page)라고 부른다.  이 랜딩페이지에 표시할 히어로 영역과 프로모션 영역을 추가해보자.

 

 히어로 영역 만들기

  히어로 영역은 웹 사이트를 대표하는 이미지를 히어로(hero)이미지라고 부르는 경향이 있다.  또한 웹 사이트를 대표하는 슬로건과 대표 메뉴로 가는 버튼을 함께 제공하는 영역을 히어로 영역이라고 한다.

import {Link} from 'react-router-dom'
import {Div} from '../../components'
import {Button} from '../../theme/daisyui'
import * as D from '../../data'

export default function Hero() {
  return (
    <div className="flex items-center p-4">
      <Div minWidth="30rem" width="30rem" maxWidth="30rem">
        <div className="flex flex-col justify-center p-4 font-bold">
          <p className="text-3xl italic text-center line-clamp-5">{D.randomSentence()}</p>
          <div className="flex justify-center mt-4 item-center">
            <Link to="/board">
              <Button className="btn-primary btn-outline">go to Board</Button>
            </Link>
          </div>
        </div>
      </Div>
      <Div
        src={D.randomImage(2000, 1600, 100)}
        className="w-full ml-4"
        minHeight="20rem"
        height="20rem"
      />
    </div>
  )
}

 

실행결과로 추가된 히어로 영역을 확인할 수 있다.

 

 

 

 프로모션 영역 만들기

  랜딩 페이지의 마케팅적인 측면을 강하게 하기 위해 고객평가의 구체적인 사례를 보여줄 때가 많기에 화면에 고객평가 데이터(customerCommet.ts) 를 만들고 고객평가 컴포넌트(customerComment.tsx)를 추가하자.

import * as C from './chance'
import * as I from './image'
export type CustomerComment = {
  uuid: string
  name: string
  jobTitle: string
  company: string
  avatar: string
  comment: string
}

export const makeCustomerComment = (
  uuid: string,
  name: string,
  jobTitle: string,
  company: string,
  avatar: string,
  comment: string
): CustomerComment => ({uuid, name, jobTitle, company, avatar, comment})

export const makeRandomCustomerComment = () =>
  makeCustomerComment(
    C.randomUUID(),
    C.randomName(),
    C.randomJobTitle(),
    C.randomCompanyName(),
    I.randomAvatar(),
    C.randomParagraphs(1)
  )

 

import type {FC} from 'react'
import type {CustomerComment as CustomerCommentType} from '../../data'
import {Div, Avatar} from '../../components'

export type CustomerCommentProps = {
  customerComment: CustomerCommentType
}

const CustomerComment: FC<CustomerCommentProps> = ({customerComment}) => {
  const {name, jobTitle, company, avatar, comment} = customerComment
  return (
    <Div
      className="relative p-2 mx-2 mt-8 rounded-lg shadow-lg boarder-2 border-primary"
      minWidth="20rem"
      width="20rem"
      minHeight="20rem"
      height="1-rem">
      <div className="absolute flex items-center justify-center w-full -top-7">
        <Avatar src={avatar} className="border-2 border-primary" />
      </div>
      <div className="flex flex-col">
        <div className="flex flex-col p-4 font-bold">
          <p>{name}</p>
          <p>{jobTitle}</p>
          <p>{company}</p>
        </div>
        <p className="mt-4">{comment}</p>
      </div>
    </Div>
  )
}
export default CustomerComment

 

 

  마지막으로 프로모션 영역에 CustomerComment 컴포넌트를 추가한다.

import {useMemo} from 'react'
import CustomerComment from './CustomerComment'
import {Div} from '../../components'
import * as D from '../../data'

export default function Promotion() {
  const comments = useMemo(() => D.makeArray(3).map(D.makeRandomCustomerComment), [])
  const children = useMemo(
    () =>
      comments.map(comment => (
        <CustomerComment key={comment.uuid} customerComment={comment} />
      )),
    [comments]
  )
  return (
    <section className="w-full mt-4">
      <h2 className="ml-4 text-5xl font-bold">What our customers say:</h2>
      <div className="flex justify-between w-full p-4">
        <Div
          width="15%"
          minWidth="15%"
          className="flex items-center justify-center text-white bg-primary">
          Your message here
        </Div>
        <div className="flex flex-wrap justify-center p-4 mt-4">{children}</div>
        <Div
          width="15%"
          minWidth="15%"
          className="flex items-center justify-center text-white item-center bg-primary">
          Your advertizement Here
        </Div>
      </div>
    </section>
  )
}

 

실행결과로 프로모션 영역에 고객평가내용이 정상적으로 표시되는 것을 확인할 수 있다.

 

 

 

커스텀 Link 컴포넌트 만들기

 현재 우리 화면에서 네비게이션의 메뉴는 선택 되었을 때 활성화 된 것처럼 표현되질 않는다.  daisyui의 버튼 컴포넌트는 버튼 활성화를 시각적으로 보여주는 btn-active 클래스를 제공하며 이를 통해 Link 컴포넌트를 구현하자.

 

import type {FC} from 'react'
import type {LinkProps as RRLinkProps} from 'react-router-dom'
import {Link as RRLink} from 'react-router-dom'

export type LinkProps = RRLinkProps & {}

export const Link: FC<LinkProps> = ({className: _classname, to, ...props}) => {
  const className = [_classname].join(' ')
  return <RRLink {...props} to={to} className={className} />
}

 

 

 네비게이션바의 기존 Link를 새로 구현한 Link 컴포넌트로 변경한다.

// import {Link} from 'react-router-dom'
import {Link} from '../../components'
export default function NavigationBar() {
  return (
    <div className="flex p-2 navbar bg-base-100">
      <Link to="/">Home</Link>
      <Link to="/board" className="ml-4">
        Board
      </Link>
    </div>
  )
}

 

 

 useResolvedPath와 usemeatch 훅 알아보기

  위의 Link 컴포넌트 구현으로는 현재 페이지에 밑줄이 생기진 않는다.  추가적으로 useResolvedPath와 useMatch 훅을 사용해 현재 페이지일 때 메뉴명에 밑줄을 그어보자.

import type {FC} from 'react'
import type {LinkProps as RRLinkProps} from 'react-router-dom'
import {useResolvedPath, useMatch} from 'react-router-dom'
import {Link as RRLink} from 'react-router-dom'

export type LinkProps = RRLinkProps & {}

export const Link: FC<LinkProps> = ({className: _classname, to, ...props}) => {
  const resolved = useResolvedPath(to)
  console.log('resolved', resolved)
  const match = useMatch({path: resolved.pathname, end: true})
  console.log('match', match)
  const className = [_classname, match ? 'btn-active' : ''].join(' ')
  return <RRLink {...props} to={to} className={className} />
}

 

실행결과로 현재 메뉴와 매치가 될 경우 값이 존재하며 매치하지 않으면 null로 표시된다. 또한 현재 선택된 메뉴 활성화 확인