React (6)

eg_kim·2024년 8월 27일

React

목록 보기
6/10
post-thumbnail

리액트에 대해 알아보자 (6)

리액트 라우터

설치방법: npm install react-router-dom

package.json 에서 설치 잘 되었는지 확인!

리액트 라우터들은 최상위 앨리먼트에 라우트 기능을 설정하는 경우가 많음.

Route

  • 주소 뒤에 불러질 내용들

Routes

  • Route들이 담겨있는 상자

NavLink

  • 뷰 라우터의 기능 중 active클래스 제공하는 기능이 있었는데 active 클래스의 개념을 가지고 있는 것이 NavLink라고 한다.
import React from 'react'
import Home from './components/Home'
import Topic from './components/Topic'
import Contact from './components/Contact'
import {Route, Routes, Nav, NavLink, BrowserRouter } from 'react-router-dom'
export default function App() {
  return (
    <BrowserRouter>
    <div className="container">
      <h2>Hello, React Router Dom!</h2>
      <ul>
      <li><a href="/">Home</a></li>
      <li><a href="topic">Topic</a></li>
      <li><a href="contact">Contact</a></li>
      </ul>

    <Routes>
      {/*<Route path='/' element={ <Home />} />*/}
      <Route index element={ <Home />} />
      <Route path='topic' element={ <Topic />} />
      <Route path='contact' element={ <Contact />} />
    </Routes>
    </div>
    </BrowserRouter>
  )
}

우선 a태그를 이용해서 경로가 이동하는 모습을 구현했다.
(새로고침 발생을 막기 위해서는 Nav를 사용하면 된다.)
index 파일 경로는 "/"라고 설정할 수도 있고, route 안에 index라고만 설명해도 메인 페이지 화면을 보여줄 수 있다.

import React from 'react'
import Home from './components/Home'
import Topic from './components/Topic'
import Contact from './components/Contact'
import {Route, Routes, Link, BrowserRouter, NavLink } from 'react-router-dom'
import './App.css'
import NotFound from './components/NotFound'

export default function App() {
  return (
    <BrowserRouter>
    <div className="container">
      <h2>Hello, React Router Dom!</h2>
      <ul>
      {/* 새로고침 발생 */}
      {/*<li><a href="/">Home</a></li>
      <li><a href="topic">Topic</a></li>
      <li><a href="contact">Contact</a></li>*/}

      {/* 새로고침 발생 X */}
      {/*<li><Link to="/">Home</Link></li>
      <li><Link to="topic">Topic</Link></li>
      <li><Link to="contact">Contact</Link></li>*/}

      {/* 새로고침 발생 X, active 클래스 제공 */}
      <li><NavLink to="/">Home</NavLink></li>
      <li><NavLink to="topic">Topic</NavLink></li>
      <li><NavLink to="contact">Contact</NavLink></li>
      </ul>

    <Routes>
      {/*<Route path='/' element={ <Home />} />*/}
      <Route index element={ <Home />} />
      <Route path='topic' element={ <Topic />} />
      <Route path='contact' element={ <Contact />} />
      <Route path='/*' element={<NotFound></NotFound>} />
    </Routes>
    </div>
    </BrowserRouter>
  )
}

//Home.js (Topic, Contact 모두 같은 내용)
import React from 'react'

export default function Contact() {
  return (
    <div>
      <h3>Contact</h3>
      <p>contact...</p>
    </div>
  )
}

//app.css (기본 코드들은 작성 생략)

a{
  text-decoration: none;
}
.active{
  color: blue;
  text-decoration: line-through;
}

(결과)

위에서도 이야기 했듯이, a링크를 사용하면 새로고침 되는 불편함이 있다.
따라서 새로고침을 막기 위해서는 <Link to="/"></Link> 를 사용하면 되고,
같은 효과인데 active class까지 함께 사용하고 싶다면 <NavLink to ="/"></NavLink> 를 사용하면 된다.

현재 선택된 내용이 menu이며 css에 active 클래스를 추가한 모습니다.

중첩 라우팅

//app.js
import React from 'react'
import Home from './components/Home'
import {Topic, Topic1, Topic2, Topic3} from './components/Topic'
import Contact from './components/Contact'
import {Route, Routes, Link, BrowserRouter, NavLink } from 'react-router-dom'
import './App.css'
import NotFound from './components/NotFound'

export default function App() {
  return (
    <BrowserRouter>
    <div className="container">
      <h2>Hello, React Router Dom!</h2>
      <ul>
      {/* 새로고침 발생 */}
      {/*<li><a href="/">Home</a></li>
      <li><a href="topic">Topic</a></li>
      <li><a href="contact">Contact</a></li>*/}

      {/* 새로고침 발생 X */}
      {/*<li><Link to="/">Home</Link></li>
      <li><Link to="topic">Topic</Link></li>
      <li><Link to="contact">Contact</Link></li>*/}

      {/* 새로고침 발생 X, active 클래스 제공 */}
      <li><NavLink to="/">Home</NavLink></li>
      <li><NavLink to="topic">Topic</NavLink></li>
      <li><NavLink to="contact">Contact</NavLink></li>
      </ul>

    <Routes>
      {/*<Route path='/' element={ <Home />} />*/}
      <Route index element={ <Home />} />
      <Route path='topic/*' element={ <Topic />} />
      <Route path='contact' element={ <Contact />} />
      <Route path='/*' element={<NotFound></NotFound>} />
    </Routes>
    </div>
    </BrowserRouter>
  )
}

//Topic.js
import React from 'react'
import { NavLink, Routes,Route } from 'react-router-dom'
import NotFound from './NotFound'


export function Topic() {
  return (
    <div>
      <h3>Topic</h3>
      <ul>
        <li><NavLink to="1">1</NavLink></li>
        <li><NavLink to="2">2</NavLink></li>
        <li><NavLink to="3">3</NavLink></li>
      </ul>

      <Routes>
        <Route index path='1' element={<Topic1 />}></Route>
        <Route path='2' element={<Topic2 />}></Route>
        <Route path='3' element={<Topic3 />}></Route>
        <Route path='/*' element={<NotFound />}></Route>
      </Routes>
    </div>
  )
}

export function Topic1() {
  return (
    <div>
      <h3>Topic1</h3>
      <p>topic1...</p>
    </div>
  )
}

export function Topic2() {
  return (
    <div>
      <h3>Topic2</h3>
      <p>topic2...</p>
    </div>
  )
}

export function Topic3() {
  return (
    <div>
      <h3>Topic3</h3>
      <p>topic3...</p>
    </div>
  )
}
//app.js
import React from 'react'
import Home from './components/Home'
import {Topic, Topic1, Topic2, Topic3} from './components/Topic'
import Contact from './components/Contact'
import {Route, Routes, Link, BrowserRouter, NavLink } from 'react-router-dom'
import './App.css'
import NotFound from './components/NotFound'

export default function App() {
  return (
    <BrowserRouter>
    <div className="container">
      <h2>Hello, React Router Dom!</h2>
      <ul>
      {/* 새로고침 발생 */}
      {/*<li><a href="/">Home</a></li>
      <li><a href="topic">Topic</a></li>
      <li><a href="contact">Contact</a></li>*/}

      {/* 새로고침 발생 X */}
      {/*<li><Link to="/">Home</Link></li>
      <li><Link to="topic">Topic</Link></li>
      <li><Link to="contact">Contact</Link></li>*/}

      {/* 새로고침 발생 X, active 클래스 제공 */}
      <li><NavLink to="/">Home</NavLink></li>
      <li><NavLink to="topic">Topic</NavLink></li>
      <li><NavLink to="contact">Contact</NavLink></li>
      </ul>

    <Routes>
      {/*<Route path='/' element={ <Home />} />*/}
      <Route index element={ <Home />} />
      <Route path='topic/*' element={ <Topic />}>
      <Route index path='1' element={<Topic1 />}></Route>
        <Route path='2' element={<Topic2 />}></Route>
        <Route path='3' element={<Topic3 />}></Route>
      </Route>
      <Route path='contact' element={ <Contact />} />
      <Route path='/*' element={<NotFound></NotFound>} />
    </Routes>
    </div>
    </BrowserRouter>
  )
}





//Topic.js
import React from 'react'
import { NavLink, Routes,Route, Outlet } from 'react-router-dom'
import NotFound from './NotFound'


export function Topic() {
  return (
    <div>
      <h3>Topic</h3>
      <p>topic...</p>
      <ul>
        <li><NavLink to="1">1</NavLink></li>
        <li><NavLink to="2">2</NavLink></li>
        <li><NavLink to="3">3</NavLink></li>
      </ul>
      <Outlet></Outlet>
    </div>
  )
}

export function Topic1() {
  return (
    <div>
      <h3>Topic1</h3>
      <p>topic1...</p>
    </div>
  )
}

export function Topic2() {
  return (
    <div>
      <h3>Topic2</h3>
      <p>topic2...</p>
    </div>
  )
}

export function Topic3() {
  return (
    <div>
      <h3>Topic3</h3>
      <p>topic3...</p>
    </div>
  )
}

(중첩 라우팅 예시)

위의 코드와 같은 결과는 아니지만, 중접 라우팅을 이용한 또 다른 예시이다.
현재 active 효과를 줄 수 있는 NavLink를 사용하여 클릭한 컴포넌트에 css 효과를 주고있고, 클릭하면 그 안에 또 다른 컴포넌트가 나타난다. 컴포넌트를 또 클릭하면 세부 내용도 확인할 수 있도록 만든 예시이다.


가상의 서버를 만들어 json 데이터 로드하기

npm install react-router

모듈 추가 npm install react-router

react-router 모듈을 추가하게 되면, spa방식의 개발환경에서 페이지를 이동하지 않고도 URL경로에 따라서 서로 다른 컴포넌트를 보여지게 하거나, 페이지가 이동하는 것처럼 보일 수 있도록 하는 기능을 쉽게 사용할 수 있다.


npm install -g json-server

백엔드 서버가 구축되지 않았을 때 json 파일을 데이터 베이스로 사용하며 가상의 API 서버를 생성할 수 있는 모듈. -g 를 추가하여 전역설치 되도록 한다.


json-server -p 4000 -w ./data/users.json

json-server -p (포트번호) -w ./data 파일이 있는 폴더명/파일명.json

json server을 실행하는 명령어.
포트번호를 입력하고 -w(watch)를 작성한 뒤 json파일이 있는 경로를 작성한다.


useLoaderData

  • 데이터를 사용할 때 fetch 대신 사용할 수 있는 hook, 라우터 안에 들어있음
  • 데이터 로딩 방식과 관련한 훅이며 기존의 useEffect를 사용하여 데이터를 로드하던 방법보다 사용자에게 빠르게 데이터를 보여줄 수 있다.

예를 들어, 블로그에서 상세페이지로 페이지를 이동해야 할 때 상세페이지의 데이터를 가져오게 되는데 useLoaderData 훅을 사용하면 페이지가 로드 되기 전에 데이터를 미리 가져올 수 있기 때문에 로딩 시간 필요 없이 빠르게 화면에 출력할 수 있다.

파라미터 방식 주소 전달

  • sub/sub1 => 경로 파라미터
  • sub?sub1 => 쿼리 파라미터

경로 파라미터는 URL 주소 뒤에 붙어서 나타내는 주소 방식이고, 쿼리 파라미터는 '키=값' 형태로 URL 주소 사이에 붙어서 나타내는 방식이다.

경로 파라미터는 주로 특정한 리소스를 식별하는데 쓰이고(ex. 상세페이지마다 다른 주소경로) 쿼리 파라미터는 주로 검색이나, 필터링 옵션 등에 사용된다.(form 태그 method의 get방식을 생각하면 된다.)

따라서 useLoaderData 훅과 주소 전달 방식을 결합하면 더욱 효율적인 데이터 전달을 할 수 있다.

중첩 라우터 및 json server 응용하기!

//app.js
import React from 'react'
import './App.css'
import { Route, createBrowserRouter, createRoutesFromElements, RouterProvider } from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'
import RootLayout from './components/RootLayout'
import HelpLayout from './components/HelpLayout'
import FAQ from './components/FAQ'
import Contact, {contactAction} from './components/Contact'
import NotFound from './components/NotFound'
import Menus, { menusLoader } from './components/Menus'
import MenusLayout from './components/MenusLayout'
import MenuDetails, {menuDetailsLoader} from './components/MenuDetails'
import MenusError from './components/MenusError'

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<RootLayout/>}>
        <Route index element={<Home/>} />
        <Route path="about" element={<About/>} />

        <Route path="help" element={<HelpLayout/>}>
          <Route path="faq" element={<FAQ/>} />
          <Route path="contact" element={<Contact/>} action={contactAction} />
        </Route>

        <Route path="menu" element={<MenusLayout/>} errorElement={<MenusError />}>
          <Route index element={<Menus/>} loader={menusLoader}></Route>
          <Route path=':id' element={<MenuDetails />} loader={menuDetailsLoader}></Route>
        </Route>

        <Route path="*" element={<NotFound />}/>
    </Route>
  )
)

export default function App() {
  return (
    <RouterProvider router={router} />

  )
}


//about.js
import React from 'react'

export default function About() {
  return (
    <div>
      <h3>About</h3>
      <p>about...</p>
    </div>
  )
}

//BreadCrumb.js
import React from 'react'
import { useLocation } from 'react-router'
import { Link } from 'react-router-dom'

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

    //console.log(location)
    //console.log(location.pathname)

    let currentLink = ""
    const crumbs = location.pathname.split('/')
    //console.log(crums)
    .filter(crumb=>{
        //console.log(crumb)
        
        return crumb !==""
        // return !crumb
    })
    .map(crumb => {
        currentLink += `/${crumb}`
        return (
            <Link to={currentLink}>{crumb}</Link>
        )
    })
  return (
    <div className="breadcrums">
      BreadCrums : {crumbs}
    </div>
  )
}


//Contact.js
import React from 'react'
import { Form, useActionData,redirect } from 'react-router-dom'

export default function Contact() {
  const data = useActionData()

  return (
    <div>
      <h3>Contact</h3>
      <Form action="/help/contact" method="post">
        <p>
          <label>
            <span>Your Email</span>
            <input type="email" name="email" required />
          </label>
        </p>

        <p>
          <label>
            <span>Message</span>
            <textarea type="message" name="message" required />
          </label>
        </p>
      <button type="submit">SUBMIT</button>

      {/* 입력된 값 && 에러 && 에러 메세지 */}
      {data && data.error && <p>{data.error}</p>}
      </Form>
    </div>
  )
}

export const contactAction = async ({request})=>{
  //console.log('request : ', request)

  const data = await request.formData()

  const submission = {
    emamil: data.get('email'),
    message: data.get('message')
  }
  
  //console.log('submission : ', submission)

  if(submission.message.length < 10){
    return {
      error: "Message must be over 10 characters long"
    }
  }
  return redirect('/')
}



//FAQ.js
import React from 'react'

export default function FAQ() {
  return (
    <div>
      <h3>FAQ</h3>
      <p>자주 묻는 질문</p>
    </div>
  )
}



//HelpLayout.js
import React from 'react'
import { NavLink,Outlet } from 'react-router-dom'
import FAQ from './FAQ'
import Contact from './Contact'

export default function HelpLayout() {
  return (
    <div>
       <h3>Customer center</h3>
      <nav>
        <ul>
          <li><NavLink to="faq" element={<FAQ></FAQ>}>FAQ</NavLink></li>
          <li><NavLink to="contact" element={<Contact/>}>contact</NavLink></li>

        </ul>
      </nav>

      <hr/>

      <Outlet></Outlet>
    </div>
  )
}


//Home.js
import React from 'react'

export default function Home() {
  return (
    <div>
      <h3>Home</h3>
      <p>home...</p>
    </div>
  )
}



//MenuDetails.js
import React from 'react'
import { useLoaderData, useParams } from 'react-router'

export default function MenuDetails() {

    const { id } = useParams()
    
    const menu = useLoaderData();
    //console.log("id : , id // menu : ", menu)

  return (
    <div>
        <h3>MenuDetails</h3>
        <p>Menu Detail : {menu.title}</p>
        <p>ID : {menu.id}</p>
        <p>TITLE : {menu.title}</p>
        <p>CONTENT : {menu.content}</p>
    </div>
  )
}

export const menuDetailsLoader = async({params}) => {
    const {id} = params
    const res = await fetch("http://localhost:4000/menu/"+id)

     //error
    //console.log(error);
    if(!res.ok){
        throw Error("다시 시도해 주십시오.")
    }

    return res.json()

    
}


//Menus.js
import React from 'react'
import {Link, useLoaderData} from 'react-router-dom'

export default function Menus() {
  const items = useLoaderData()
  return (
    <div>
      {
        items.map(item => (
          <p key={item.id}>
            <Link to={item.id.toString()}>{item.title}</Link>
          </p>
      ))
      }
    </div>
  )
}

export const menusLoader = async () => {
    const res = await fetch("http://localhost:4000/menu")
    //error
    //console.log(error);
    if(!res.ok){
        throw Error("다시 시도해 주십시오.")
    }
    return res.json()
}

//MenusError.js
import React from 'react'
import { Link,useRouteError } from 'react-router-dom'

export default function MenusError() {
    const error = useRouteError
    console.log("ERROR : ", error)
  return (
    <div>
      <h3>Error!</h3>
      <p>{error.Message}</p>
      <Link to="/">Go to home</Link>
    </div>
  )
}


//MenusLayout.js
import React from 'react'
import { Outlet } from 'react-router'

export default function MenusLayout() {
  return (
    <div>
      <h3>Menu</h3>
      <p>Coffee,Tea,Bread,...</p>
      <Outlet></Outlet>
    </div>
  )
}




//NotFound.js
import React from 'react'
import { NavLink } from 'react-router-dom'

export default function NotFound() {
  return (
    <div>
      <h3>요청하신 페이지를 찾을 수 없습니다.</h3>
      <p>입력된 주소를 다시 한 번 확인해주세요.</p>
      <p><NavLink to="/">Go MainPage</NavLink></p>
    </div>
  )
}



//RootLayout.js
import React from 'react'
import { Outlet,NavLink } from 'react-router-dom'
import BreadCrums from './BreadCrums'

export default function RootLayout() {
  return (
    <div>
        <BreadCrums></BreadCrums>
      <h2>React Router Dom</h2>
      <nav>
        <ul>
          <li><NavLink to="/">Home</NavLink></li>
          <li><NavLink to="about">About</NavLink></li>
          <li><NavLink to="help">Help</NavLink></li>
          <li><NavLink to="menu">Menu</NavLink></li>
        </ul>
      </nav>

      <hr/>

      <Outlet></Outlet>
    </div>
  )
}


//Topic.js
import React from 'react'
import { NavLink, Routes,Route, Outlet } from 'react-router-dom'
import NotFound from './NotFound'


export function Topic() {
  return (
    <div>
      <h3>Topic</h3>
      <p>topic...</p>
      <ul>
        <li><NavLink to="1">1</NavLink></li>
        <li><NavLink to="2">2</NavLink></li>
        <li><NavLink to="3">3</NavLink></li>
      </ul>
      <Outlet></Outlet>
      
    </div>
  )
}

export function Topic1() {
  return (
    <div>
      <h3>Topic1</h3>
      <p>topic1...</p>
    </div>
  )
}

export function Topic2() {
  return (
    <div>
      <h3>Topic2</h3>
      <p>topic2...</p>
    </div>
  )
}

export function Topic3() {
  return (
    <div>
      <h3>Topic3</h3>
      <p>topic3...</p>
    </div>
  )
}

(결과)

Outlet의 역할

중첩된 라우트의 콘텐츠를 렌더링하는 위치를 지정하는 컴포넌트

라우트를 중첩하여 정의할 때 공통적인 UI 요소는 부모 컴포넌트에 정의하고, 자식 라우트는 부모 컴포넌트 내에서 Outlet을 통해 정의된다.
마치 provider 처럼 부모 컴포넌트 내에 Outlet을 배치하여 자식 컴포넌트를 배치할 수 있다.

BreadCrumbs는 웹사이트에서 상세페이지로 넘어갔을 때 경로를 알려주는 부분의 역할을 한다.
예를 들어

(출처: 국세청)

이렇게 홈에서부터(지정한 경로부터) 어느 카테고리의 하위 컴포넌트에 속해있는지 경로를 보여주는 방식이다.

그리고 Menu를 클릭하게 되면

이렇게 error!을 마주할 수 있는데
이 경우에는 MENU의 데이터가 json 파일에 담겨있기 때문에 일반 서버에서는 확인할 수 없기 때문이다.
따라서 json 파일을 보여줄 수 있는 가상의 경로가 필요한데, 이때 필요한 것이 위에서 언급한 json-server이다.

그런데 npm start를 하고 나서 ctrl+c -> json-server -p (포트번호) -w ./data 파일이 있는 폴더명/파일명.json 을 반복적으로 시도했는데 계속 같은 Error!만 보이고, json 서버에서는 json 파일에 담긴 데이터만 확인할 수 있었다.

이때 해결한 방법이 새로운 터미널 하나를 더 여는 것! 이었다.

json 데이터가 불러와지는지 확인하는 방법!

  1. npm start로 개발중인 가상 서버를 연다.

  1. 터미널 하나를 더 실행한 뒤 json server를 여는 명령어를 입력한다.

  1. 가상서버를 열고, 빈 화면이 나타나면 뒤에 파일명을 붙인다.

(결과)

json파일에 담긴 데이터 확인!

  1. npm start로 열었던 가상서버로 전환하여 새로고침을 하고, 데이터가 담긴 컴포넌트를 클릭한다.

컴포넌트 안에 담겨있는 하위 컴포넌트를 확인할 수 있고, 여기에서 메뉴를 하나씩 클릭하면

json 파일에서 데이터가 정상적으로 출력되는 모습을 확인할 수 있다.

오류 해결!

profile
오늘의 공부 기록📝

0개의 댓글