완벽 가이드 - SPA

primav·2024년 12월 11일

React

목록 보기
29/35
post-thumbnail

📌 라우팅

라우팅이란?

라우팅은 사용자가 요청한 URL을 분석해 그 요청에 맞는 자원을 반환하거나 특정 동작을 수행하도록 연결하는 과정이다. 주로 웹 애플리케이션에서 클라이언트가 특정 URL로 접근하면 서버나 프론트엔드가 이에 대한 응답을 처리하기 위해 라우팅을 설정한다.

❗️ URL 뒤의 엔드 포인트에 따라 각 페이지를 불러온다.

라우팅 특징

  1. 서버 사이드 라우팅
  • 서버가 요청 URL을 분석하고 이에 적합한 HTML, 데이터, 또는 파일을 반환하는 방식이다.
  • 예를 들어 /home 요청이 들어오면 서버가 home.html을 반환한다.
  1. 클라이언트 사이드 라우팅
  • SPA에서 주로 사용되며, 브라우저 내에서 URL을 변경하고 해당 상태를 렌더링하는 방식이다.
  • 페이지 리로드 없이 동적으로 콘텐츠를 업데이트한다.
  • 예를 들어 /profile 요청이 들어오면 JavaScript가 동적으로 Profile 컴포넌트를 렌더링한다.

📌 SPA

SPA란?

SPA(Single Page Application)는 단일 HTML 페이지로 구성된 웹 애플리케이션이다. 페이지 이동 없이 브라우저에서 동적으로 콘텐츠를 갱신하며 사용자 경험을 개선한다.

특징

  1. 단일 페이지 로드
  • 최초 로드 시 필요한 리소스를 모두 가져오고 이후에는 데이터만 교환한다.
  • 전체 페이지를 새로 로드하지 않는다.
  1. 클라이언트 사이드 렌더링 (CSR)
  • JavaScript를 이용해 브라우저에서 HTML을 생성하고 콘텐츠를 동적으로 갱신한다.
  1. 빠른 사용자 경험
  • 서버 요청이 적고 콘텐츠가 빠르게 업데이트된다.
  • 네비게이션을 클릭할 때 로딩 없이 화면이 즉시 전환된다.

📌 react-router-dom

이 패키지를 프로젝트에 설치함으로써 우리는 URL 변경을 감지하고 다양한 콘텐츠를 로딩할 수 있게 된다.

npm install react-router-dom

🎯 라우팅

1. 라우트 정의

URL과 경로에 대해 어떤 컴포넌트가 로딩되어야 하는지 정의한다.

예를 들어 https://naver.com/pages 라는 URL 이 있을 때 naver.com이 도메인이고, /pages 부분을 경로라고 한다. (path)

import { createBrowserRouter, RouterProvider } from "react";

const router = createBrowserRouter([ // 라우터 생성
  { path: "/", element: <HomePage /> }, // 경로 & 경로에 따른 컴포넌트 지정
  { path: "/products", element: <ProductsPage /> },
]);

function App() {
  return <RouterProvider router={router} />; // 라우터 객체 연결 & 반환
}

2. 라우터 활성화

라우터를 활성화하고 라우트 정의를 로딩한다.

3. 라우팅 연결

로딩하려는 컴포넌트들이 있는지 확인하고 그 페이지들 간에 이동할 수단을 제공했는지 확인한다.

URL의 path 를 변경하면 그 경로에 지정된 페이지로 바뀌도록 위에서 Router를 설정해주었다.
그렇다면 사용자들은 이 path를 어떻게 이용하여 페이지를 이동할까?

❌ 기존 방식
<p>
   Go to <a href="/products">the products pages</a>
</p>

위와 같이 a 태그를 사용한 페이지 이동 방식은 URL을 변경하지만, 페이지 자체를 새로고침하기 때문에 SPA 장점을 활용하지 못한다.

그 대신 react-router-dom 에서 지원하는 Link & to 를 사용하면 페이지의 새로고침 없이 URL의 path만 변경하여 페이지를 전환할 수 있다.

➡️ 새로운 HTTP 요청을 보내지 않고 클라이언트 사이드에서 페이지를 렌더링하는 것이다!

SPA 방식
<p>
   Go to <Link to="/products">the products pages</Link>
</p>
// 빠르고 부드러운 페이지 전환을 가능하게 한다.

프로젝트를 SPA 형식으로 만드려면 어떻게 해야할까?

  1. 라우터 설정 - react-router-dom 같은 라이브러리를 사용해 라우터를 설정해야 한다.
  2. 클라이언트 사이드 렌더링 - 페이지 전환 시 새로운 요청 없이 기존 HTML을 업데이트 하도록 해야한다.

➡️ 라우터에서 페이지를 SPA로 지정한다고만 할 수 없고 라우터 설정 + 클라이언트 사이드 렌더링 이 함께 동작해야 SPA이다.

위에서 말한 a 태그는 이러한 구조와 동작에 맞지 않으므로 SPA의 장점을 활용하지 못한다는 것이다.

🎯 Outlet - 중첩된 라우트

Outlet은 React Router에서 중첩된 라우트를 렌더링하는 데 사용되는 특별한 컴포넌트이다. 부모 라우트(최상위 라우트)가 정의되어 있는 컴포넌트 내부에서 자식 라우트를 렌더링할 위치를 지정할 때 사용한다.

언제 Outlet을 사용할까?

  • 공통 레이아웃
    네비게이션 바, 푸터, 사이드 바처럼 모든 페이지에서 공통적으로 보여야 하는 요소가 있을 때 사용한다.

  • 중첩된 라우트
    특정 경로 아래에 또 다른 라우트를 중첩해 구조화하고 싶을 때 유용하다.
    ex)/products 아래 /products/details 와 같은 하위 경로를 관리하고자 할 때 사용

작동 방식

  1. 부모 라우트는 Outlet을 통해 자식 라우트가 렌더링될 위치를 제공한다.
  2. 자식 라우트는 부모 라우트의 Outlet에 렌더링되며, 부모의 요소와 함께 표시된다.

➡️ 부모와 자식 라우트가 분리된 역할을 가지면서도 하나의 레이아웃을 공유할 수 있도록 돕는다.

1. 라우터 설정

📁 App.js

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />, // 최상위 라우트
    children: [ // 중첩되는 자식 라우트
      { path: "/", element: <HomePage /> },
      { path: "/products", element: <ProductsPage /> },
    ],
  },
]);

2. Root 컴포넌트에서 Outlet 사용

최상위 레이아웃 컴포넌트에서 Outlet을 추가해 자식 라우트가 렌더링될 위치를 지정한다.

📁 Root.js

 import { Outlet } from "react-router-dom";

function RootLayout() {
  return (
    <>
      <h1>Root Layout</h1>
      <Outlet /> // 자식 라우트 렌더링 될 자리 
    </>
  );
}

export default RootLayout;

3. 네비게이션 컴포넌트 작성

공통으로 사용할 최상위 레이아웃 컴포넌트에 들어갈 네비게이션 바를 따로 작성한다.

📁 MainNavigation.js

import { Link } from "react-router-dom";

function MainNavigation() {
  return (
    <header>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/products">Products</Link>
        </li>
      </ul>
    </header>
  );
}

export default MainNavigation;

4. 공통 레이아웃 추가 (네비게이션 바)

공통적으로 보여야 하는 네비게이션 바를 RootLayout에 추가한다.

📁 Root.js

import { Outlet } from "react-router-dom";
import MainNavigation from "../components/MainNavigation";

function RootLayout() {
  return (
    <>
      <MainNavigation />
      <Outlet />
    </>
  );
}

export default RootLayout;

💡 결론

Outlet은 SPA에서 공통 UI를 유지하며 각 페이지를 동적으로 렌더링할 수 있게 해주는 핵심적인 도구이다.
부모 라우트와 자식 라우트 간의 역할을 명확히 구분한다.

🎯 errorElement

errorElementReact Router에서 특정 라우트나 해당 라우트의 하위 라우트에서 에러가 발생했을 때 렌더링할 컴포넌트를 지정하는 옵션이다.

ex) 잘못된 엔드 포인트로 URL에 접속했을 때 띄우는 페이지

사용 목적

  • 에러 관리
  • 일관된 UI 유지
  • 에러별 맞춤 컴포넌트 제공 가능

1. 라우터 설정에 errorElement 추가

📁 error.js

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />, // 에러 발생 시 렌더링될 컴포넌트
    children: [
      { path: "/", element: <HomePage /> },
      { path: "/products", element: <ProductsPage /> },
    ],
  },

2. 에러 페이지 컴포넌트 작성

 import MainNavigation from "../components/MainNavigation";

export default function ErrorPage() {
  return (
    <>
      <MainNavigation /> // 네비게이션 (부모 컴포넌트 - Outlet)
      <main>
        <h1>An error occurred!</h1>
        <p>Could not find this page</p>
      </main>
    </>
  );
}

Link 를 써서 페이지를 이동하는 방법은 css 를 설정한다면, 클릭하거나 마우스를 가져다놓았을 때의 디자인은 쉽게 할 수 있다. 하지만 현재 페이지에 접속해있을 동안 그 페이지 인 것을 알 수 있게 할 방법이 없다.

이때 NavLinkReact Router에서 제공하는 컴포넌트로, 네비게이션에서 현재 활성화된 링크를 강조하거나 스타일을 적용할 때 사용된다. 기본적인 페이지 이동은 Link로 처리할 수 있지만, NavLink는 활성 상태를 감지해 네비게이션 UI에서 현재 페이지를 사용자에게 표시할 수 있는 기능을 제공한다.

❗️ NavLinkto 속성에 지정된 경로가 현재 URL과 매칭되면 기본적으로 active 라는 클래스가 자동으로 추가된다.

작동방식

  1. NavLink 는 현재 URL 경로와 to 속성 값을 비교해 활성 상태를 결정한다.
  2. 활성화된 경우 기본적으로 active 클래스가 적용된다.
  3. className이나 style 속성을 콜백으로 설정하면 isActive 값을 활용해 커스텀 동작을 구현할 수 있다.

👀 활성화된 경우 active 클래스가 적용된다는 것이 무슨 말일까?

active 클래스가 적용된다는 것은, React RouterNavLink 컴포넌트가 현재 페이지 URL과 to 속성에 설정된 경로가 일치하면, 해당 <NavLink> 엘리먼트에 자동으로 active라는 CSS 클래스가 추가된다는 의미이다. 이 클래스는 개발자가 스타일링할 때 활용할 수 있다.

예를 들면,

  <nav> // 기본 네비게이션 링크
      <NavLink to="/" end>Home</NavLink>
      <NavLink to="/products">Products</NavLink>
  </nav>
  
  <nav> // 현재 URL이 /이면
    <a href="/" class="active">Home</a> // active 클래스 추가됨 --> a.active로 사용
    <a href="/products">Products</a>
  </nav>

👀 경로가 /인 경우, 모든 링크가 / 로 시작되어 활성화 되는데 어떻게 해야할까?

NavLink의 기본 동작은 URL 경로가 to 속성에 지정된 값과 부분적으로만 일치해도 active 클래스를 추가하는 것이다.

따라서 아래와 같은 상황에서 /products 로 끝나는 페이지에 접속을 하면 두 링크가 모두 활성화가 되는 것이다.

이런 경우 NavLink에서 지원하는 end 속성을 이용하여 이 path가 마지막에 오는지를 확인하면 된다.
예를 들어 / 는 뒤에 다른 경로가 더 올 수 있으므로 end=true인지를 확인해야 한다.
(/products 는 뒤에 더 붙지 않으므로 확인할 필요 없음)

 import { NavLink } from "react-router-dom";
 import classes from "./MainNavigation.module.css";
 
<NavLink
    to="/"
    className={({ isActive }) =>
      isActive ? classes.active : undefined
    }
    end
  >
    Home
</NavLink>

<NavLink
     to="/products"
     className={({ isActive }) =>
     isActive ? classes.active : undefined
     }
     >
      Products
</NavLink>

🎯 useNavigate

useNavigateReact Router에서 제공하는 훅으로, 프로그래밍 방식으로 페이지 이동을 수행할 수 있게 해준다. 사용자가 버튼을 클릭하거나 특정 이벤트가 발생했을 때 코드로 경로를 변경하는 데 유용하다.

동작 원리

  • useNavigate 는 라우터에서 제공되는 내장 함수 navigate 를 반환한다.
  • 이 함수는 to 속성으로 지정한 경로로 이동하며, 새로고침 없이 URL과 UI를 반환한다.

1. useNavigate로 navigate 함수 가져오기

  • 경로 이동: navigate("/path")
  • 뒤로가기: navigate(-1)
  • 앞으로가기: navigate(1)
const navigate = useNavigate();

2. 이벤트 핸들러에서 navigate 호출

지정한 경로로 사용자를 이동시킨다.

function navigateHandler() {
  navigate("/products");
}

3. 버튼 클릭으로 경로 변경

사용자가 버튼을 클릭하면 navigateHandler 함수가 실행되고, UI가 /products 페이지로 변경된다.

<button onClick={navigateHandler}>Navigate</button>
 import { Link, useNavigate } from "react-router-dom";

function HomePage() {
  const navigate = useNavigate();

function navigateHandler() {
  navigate("/products");
}
  
<button onClick={navigateHandler}>Navigate</button>

🎯 useParams

useParamsReact Router DOM에서 제공하는 훅으로, URL 경로에 정의된 동적 경로 매개변수를 읽어오는 데 사용된다. 동적 경로를 기반으로 특정 데이터를 렌더링하거나 조건에 따라 UI를 변경할 수 있다.

언제 사용할까?

  • 동적 경로가 필요한 경우 (개별 페이지)
    (ex: 특정 상품 페이지/products/:productId, 사용자 프로필 페이/users/:userId)
  • URL 경로에 포함된 정보를 기반으로 데이터를 가져오거나 UI를 변경해야 할 때

1. 라우터에 동적 경로 정의

동적 경로는 콜론(:)을 사용해 정의한다. 예를 들어, /products/:productIdproductId를 동적으로 처리하도록 설정한 경로이다.

(이 :productId는 특정 값(p1, p2 등)에 대응된다.)

📁 App.js

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    children: [
      { path: "/", element: <HomePage /> },
      { path: "/products", element: <ProductsPage /> },
      { path: "/products/:productId", element: <ProductDetailPage /> }, // 뒤에 오는 path 가 동적임을 react-router-dom에게 알려줌 :/productId 로!
    ],
  },
]);

2. 동적 링크 생성

Link를 사용해 동적으로 생성된 경로로 이동할 수 있는 링크를 만든다. 경로에 포함된 :productId 는 데이터에서 동적으로 설정된다.

📁 Product.js

 const PRODUCTS = [ // 백엔드에서 받아올 수도 있음
  { id: "p1", title: "Product 1" },
  { id: "p2", title: "Product 2" },
  { id: "p3", title: "Product 3" },
 ];

<ul>
  {PRODUCTS.map((product) => (
    <li key={product.id}> // 경로로 이동
      <Link to={`/products/${product.id}`}>{product.title}</Link>
    </li>
  ))}
</ul>

3. useParams 이용하여 경로 가져오기

useParams를 이용해 URL의 동적 경로에 포함된 값을 읽는다. useParams는 URL에 지정된 매개변수의 키와 값을 객체 형태로 반환한다.

❗️ 값을 읽어올 때는 라우터에서 정의한 식별자 이름을 그대로 써야 한다!!
path="/products/:productId"로 정의했다면, params.productId로 값을 가져와야 한다.
이름이 일치하지 않으면 값을 읽을 수 없다.

❗️ 매개변수 값은 문자열로 반환된다.
숫자를 사용해야 할 경우 parseInt 같은 변환이 필요하다.

📁 ProductDetail.js

import { useParams } from "react-router-dom";

export default function ProductDetailPage() {
  const params = useParams(); // { productId: 'p1' }

  return (
    <> // 라우터에서 정의한 식별자 그대로 써야함!!
      <h1>{params.productId} Product Details!</h1>
    </>
  );
}

📌 각 방법은 언제 사용할까?

  • 사용 시점:
    내비게이션 메뉴에서 현재 활성화된 경로(페이지)를 강조해야 할 때 사용.
  • 예시:
    헤더나 사이드바에 있는 메뉴 버튼(탭)에 적합.
  • 사용 시점:
    간단히 사용자가 클릭하면 페이지를 이동해야 할 때 사용.
    현재 경로를 기반으로 상태를 표시할 필요가 없는 경우 적합.
  • 예시:
    콘텐츠나 목록 항목에서 다른 페이지로 이동하는 링크.

useNavigate

  • 사용 시점:
    코드 로직(조건, 이벤트 핸들러 등)을 통해 경로를 동적으로 변경해야 할 때 사용.
  • 예시:
    로그인 성공 후 대시보드로 리다이렉트하거나, 특정 조건에 따라 페이지를 이동시킬 때.

💡 요약

  • 내비게이션 바: NavLink 사용 (현재 경로 강조 필요)
  • 단순 링크: Link 사용
  • 동적 경로 이동: useNavigate 사용 (버튼 클릭, 조건부 이동 등)

📌 절대 경로와 상대 경로

📁 app.js

const router = createBrowserRouter([
  {
    path: "/root", // 부모
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    children: [
      { path: "", element: <HomePage /> }, // 자식 1
      { path: "products", element: <ProductsPage /> }, // 자식 2
      { path: "products/:productId", element: <ProductDetailPage /> }, // 자식 3
    ],
  },
]);

절대 경로

  • 구문: /products/ 처럼 슬래시(/)로 시작하는 경로.

  • 특징:

    • 도메인 이름 바로 뒤에 붙는다.
    • 현재 활성 경로와 상관없이 항상 특정한 위치로 이동한다.

❗️ 절대 경로는 도메인 이름 바로 뒤에 추가되고, 현재 활성인 경로 뒤에 추가되지 않는다!

 <Link to={`/products/${product.id}`}>{product.title}</Link>/root/home ➡️ /products/${product.id}

따라서 이전 페이지로 가고싶을 때, 아래와 같이 ..로 바로 전 페이지로 이동 가능하다.
ex) /root/products/p1../root/products/

📁 Product.js

<Link to={`/products/${product.id}`}>{product.title}</Link>

📁 ProductDetail.js

<Link to="..">Back</Link> // 한 단계 이전으로

상대 경로

  • 구문: products/처럼 슬래시(/) 없이 작성한 경로.
  • 특징:
    • 현재 활성 경로에 상대적으로 경로를 추가한다.
    • 현재 경로의 뒤에 덧붙여지는 방식으로 작동한다.
 <Link to={product.id}>{product.title}</Link>/root/products/ ➡️ /root/products/${product.id}

상대 경로는 이전 페이지로 가고싶을 때, 아래와 같이 ..를 하면 상위 경로로 돌아간다.
건너온 경로들을 무시하고 아예 라우터에 지정한 부모 경로로 돌아가는 것이다.
ex) /root/products/p1../root

이 문제점을 막기 위해 상대 경로에는 relative='path' 라는 속성이 있다.
이것을 지정하면 한단계 이전 경로로도 돌아갈 수 있다.
(지정 안하면 relative='route' : 이게 기본 값)

📁 Product.js

<Link to={product.id}>{product.title}</Link> 

📁 ProductDetail.js

<Link to="..">Back</Link> // 한 단계 이전으로

상대 경로 vs 절대 경로

경로 유형절대 경로상대 경로
작성 방식/products/products/
이동 기준도메인 이름 뒤에 바로 추가됨현재 활성 경로 뒤에 경로가 추가됨
사용 사례명확한 경로로 이동이 필요할 때현재 위치를 기준으로 경로를 추가할 때
..(상위)한단계 이전으로 이동- 기본 : 부모 라우터로 이동
- relative="path" 속성 지정 시 한 단계 이전으로 이동 가능

👀 그럼 언제 절대 경로를 사용하고 언제 상대 경로를 사용할까?

  • 절대 경로 - 간단한 네비게이션 메뉴
  • 상대 경로 - 동적 데이터 기반 작업이나 중첩 라우트

인덱스 라우트

index: true는 기본적으로 부모 경로의 기본 페이지(Default Page)를 지정할 때 사용하는 옵션이다.
루트 또는 특정 부모 경로에서 별도의 경로를 지정하지 않아도 자동으로 렌더링되는 페이지를 설정할 수 있다.

아래와 같이 기본 페이지에서는 경로가 없어 path: "" 라고 지정되는 경우에 index: true 속성을 사용하여 루트 페이지가 기본 페이지라고 알려준다.

이렇게 작성하면 기본 페이지가 어디인지 명확히 알 수 있으므로 권장되는 방법이다.

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    children: [
       // { path: "", element: <HomePage /> },{ index: true, element: <HomePage /> }, // path: ""
      { path: "products", element: <ProductsPage /> },
      { path: "products/:productId", element: <ProductDetailPage /> }, // 뒤에 오는 path 가 동적임을 react-router-dom에게 알려줌 :/productId 로!
    ],
  },
]);

📌 데이터 불러오기

⭐️ 코드 꼭 외워두기 !! ⭐️

맨날 헷갈리는 부분
1. useEffect
2. response.json()
3. async & await
4. await response.json()
5. 함수 호출
6. []

 const [isLoading, setIsLoading] = useState(false);
 const [fetchedEvents, setFetchedEvents] = useState();
 const [error, setError] = useState();

 useEffect(() => {
    async function fetchEvent() {
      setIsLoading(true);
      const response = await fetch("http://localhost:8080/events");

      if (!response.ok) {
        setError("Fetching events failed");
      } else {
        const resData = await response.json();
        setFetchedEvents(resData.events);
      }
      setIsLoading(false);
    }

    fetchEvent();
  }, []);

📌 loader()

loader란?

해당 라우터가 렌더링 되기 전에 loader에 있는 함수가 먼저 실행되는 것

React Router의 loader는 특정 경로를 렌더링하기 전에 데이터를 미리 가져오는 데 사용된다.
라우터가 렌더링되기 전에 loader가 실행되며, 이 데이터를 컴포넌트에서 사용 가능하다.

loader 특징

  1. 데이터 프리패칭
  • 컴포넌트가 렌더링되기 전에 필요한 데이터를 미리 가져온다.
  • 페이지가 렌더링될 때 로딩 상태 없이 데이터를 즉시 사용할 수 있다.
  1. 비동기 지원
  • loaderasync 함수로 작성 가능하며, promise를 반환해 비동기 데이터를 가져온다.
  1. 에러 처리
  • loader에서 에러가 발생하면 해당 라우트의 errorElement가 표시된다.
  1. Context 전달
  • useLoaderData()를 사용하여 loader가 반환한 데이터를 컴포넌트에서 사용할 수 있다.

사용 방법

{ path: "/", element: <EventsPage />, loader: () => {} }

loader 실행 조건

  • 사용자가 라우트에 진입할 때
    ex) 사용자가 /events 경로로 직접 이동하거나, 링크를 클릭해 해당 경로로 이동할 때

  • 라우트 간 탐색 시
    다른 라우트에서 이동하며 새로운 라우트가 활성화될 때

  • 프로그램적으로 탐색할 때
    useNavigate 또는 navigate를 통해 경로를 변경할 때

  • 페이지 새로고침 시
    사용자가 브라우저를 새로고침하면 해당 경로와 연결된 loader가 다시 실행

데이터 가져오기

EventsPage 에서 데이터를 불러오는 비동기 코드를 App.js에서 loader 안에 작성한다.
이렇게 하면 EventsPage가 렌더링되기 전에 데이터를 미리 가져올 수 있고, 그 데이터를 EventsPage에 쉽게 보낼 수 있다.

{
    index: true,
    element: <EventsPage />,
    loader: async () => {
      const response = await fetch("http://localhost:8080/events");
      
      if (!response.ok) {
        //
      } else {
        const resData = response.json();
        return resData.events;
      }
    },
},

useLoaderData 로 불러온 데이터 활용하기

가장 가까운 loader 데이터에 엑세스 하기 위해 사용한다.
위의 loader 에서 리턴한 데이터들을 받아와서 활용한다.

1. 데이터 받아서 컴포넌트로 보내기

📁Events.jsx

import { useLoaderData } from 'react-router-dom';

function EventsPage() {
  const events = useLoaderData();
  return <EventsList events={events} />;
}

2. 필요한 컴포넌트에서 바로 받기

📁EventsList.jsx

import { useLoaderData } from 'react-router-dom';

function EventsPage() {
  const events = useLoaderData();
  return 
  	<>
    {events.map((event) => {...})}; 
    </>;
}

Before vs After

✔️ Before

<// 컴포넌트 안에서 useEffect로 데이터를 불러와서 사용한다.
// 이렇게 되면 다른 컴포넌트에서 데이터를 사용할 때 다시 보내주어야 하므로 불편함

📁 App.js
function App() {
  const router = createBrowserRouter([
    {
      path: "/",
      element: <RootLayout />,
      children: [
        { index: true, element: <HomePage /> },
        {
          path: "events",
          element: <EventLayout />,
          children: [
			{ path: "", element: <EventsPage /> },
            { path: ":eventId", element: <EventDetailPage /> },
            { path: "new", element: <NewEventPage /> },
            { path: ":eventId/edit", element: <EditEventPage /> },
          ],
        },
      ],
    },
  ]);

  return <RouterProvider router={router} />;
}

export default App;

📁 Events.js

import { useEffect, useState } from "react";

import EventsList from "../components/EventsList";

function EventsPage() {
  const [isLoading, setIsLoading] = useState(false);
  const [fetchedEvents, setFetchedEvents] = useState();
  const [error, setError] = useState();

  useEffect(() => {
    async function fetchEvent() {
      setIsLoading(true);
      const response = await fetch("http://localhost:8080/events");

      if (!response.ok) {
        setError("Fetching events failed");
      } else {
        const resData = response.json();
        setFetchedEvents(resData.events);
      }
      setIsLoading(false);
    }

    fetchEvent();
  }, []);

  return (
    <>
      <div style={{ textAlign: "center" }}>
        {isLoading && <p>Loading...</p>}
        {error && <p>{error}</p>}
      </div>
      {!isLoading && fetchedEvents && <EventsList events={fetchedEvents} />}
    </>
  );
}

export default EventsPage;

✔️ After

// 라우터 안에서 데이터를 불러옴
// 필요한 컴포넌트에 라우터의 loader를 통해 데이터를 각각 보내므로 쉽게 데이터 사용 가능

📁 App.js

function App() {
  const router = createBrowserRouter([
    {
      path: "/",
      element: <RootLayout />,
      children: [
        { index: true, element: <HomePage /> },
        {
          path: "events",
          element: <EventLayout />,
          children: [
            {
              index: true,
              element: <EventsPage />,
              loader: async () => {
                const response = await fetch("http://localhost:8080/events");

                if (!response.ok) {
                  //
                } else {
                  const resData = response.json();
                  return resData.events;
                }
              },
            },
            { path: ":eventId", element: <EventDetailPage /> },
            { path: "new", element: <NewEventPage /> },
            { path: ":eventId/edit", element: <EditEventPage /> },
          ],
        },
      ],
    },
  ]);

  return <RouterProvider router={router} />;
}

export default App;


📁 Events.js

import { useLoaderData } from "react-router-dom";

import EventsList from "../components/EventsList";

function EventsPage() {
  const events = useLoaderData();
  return <EventsList events={events} />;
}

export default EventsPage;

loader 를 사용하면 모든 컴포넌트에서 데이터를 사용할 수 있을까?

정답은 아니다.
❗️ 낮은 레벨에서 정의된 라우트에서 받아온 데이터는 그 상위 컴포넌트에서는 사용할 수 없다!!

따라서 아래 코드에서 예시를 들면 EventLayout을 루트로 하는 컴포넌트에서는 아래의 방식대로 하면 언제든지 데이터를 사용할 수 있다.

useLoaderData 로 데이터 사용하기

  • import { useLoaderData } from 'react-router-dom'
  • const data = useLoaderData()

하지만 RootLayout에 있는 Homepage 등 그 상위 컴포넌트에서는 데이터를 꺼내올 수 없다.

const router = createBrowserRouter([
    {
      path: "/",
      element: <RootLayout />,
      children: [
        { index: true, element: <HomePage /> },
        {
          path: "events",
          element: <EventLayout />,
          children: [
            {
              index: true,
              element: <EventsPage />,
              loader: async () => {
                const response = await fetch("http://localhost:8080/events");

                if (!response.ok) {
                  //
                } else {
                  const resData = await response.json();
                  return resData.events;
                }
              },
            },
            { path: ":eventId", element: <EventDetailPage /> },
            { path: "new", element: <NewEventPage /> },
            { path: ":eventId/edit", element: <EditEventPage /> },
          ],
        },
      ],
    },
  ]);

loader() 코드를 저장할 위치

하지만 loader 함수를 App.js 에서 익명함수로 사용하면 App.js가 방대해진다는 단점이 있다. 이를 위해 다른 곳에 함수를 지정하고 App.js에서 함수를 가져오는 방식을 취할 수 있다.

❗️ 필요한 컴포넌트에 더 가까운 곳에 있는 함수에 아웃소싱 해서 App.js에서 가져와 사용한다면 더 간단하게 만들 수 있다.

📁 App.js

import { loader as eventsLoader } from "./pages/EventsPage";
{
    index: true,
    element: <EventsPage />,
    loader: eventsLoader(), // 함수 불러옴
}
    
📁 EventsPage.js
  
import { useLoaderData } from "react-router-dom";

import EventsList from "../components/EventsList";

function EventsPage() {
  const events = useLoaderData();
  return <EventsList events={events} />;
}

export default EventsPage;

export async function loader() { // 아웃소싱
  const response = await fetch("http://localhost:8080/events");

  if (!response.ok) {
    //
  } else {
    const resData = await response.json();
    return resData.events;
  }
}

💡 결론

  • loader 는 비동기 데이터 로드와 초기화를 처리하는 도구이다.
  • 라우터가 활성화되기 직전에 실행되어 컴포넌트에 필요한 데이터를 사전에 준비한다.
  • 데이터를 미리 로드하여 페이지 렌더링 속도를 높이고 사용자 경험을 향상시키는데 적합하다.
  • ❗️ 데이터가 페이지에 필수적인 경우 loader를 적극적으로 사용하자!!

📌 Navigation

Navigation은 애플리케이션 내에서 사용자가 페이지 간 이동(라우팅)을 할 수 있도록 관리하는 시스템이다.
React Router와 같은 라이브러리는 이러한 네비게이션 기능을 제공하여 SPA(Single Page Application)에서도 부드럽고 직관적인 사용자 경험을 구현할 수 있다.

주요 역할

  1. 페이지 전환 관리
  • 사용자가 링크를 클릭하거나, 특정 경로를 입력했을 때 페이지를 전환한다.
  • 기존의 서버 요청 방식 대신, 클라이언트에서 페이지 전환이 이루어져 더 빠른 응답 속도를 제공한다.
  1. 데이터 로드
  • 페이지 전환 시, 해당 페이지에 필요한 데이터를 미리 로드하거나, 전환 도중 데이터를 로드한다.
  • React Router에서는 loader()를 활용하여 데이터를 로드할 수 있다.
  1. 상태 추적
  • 현재 애플리케이션의 라우트 상태(idle, loading, submitting)를 관리한다.
  • 이를 통해 로딩 중 로딩 스피너 표시, 제출 중 버튼 비활성화 등의 UI를 동적으로 처리할 수 있다.

상태 추적 (navigation.state)

  • idle : 라우터 전환이 일어나지 않음
  • loading : 라우터 전환이 일어나고 데이터를 로딩 중
  • submitting : 라우터 전환이 일어나고 데이터를 제출
import { useNavigation } from "react-router-dom";

function App() {
  const navigation = useNavigation(); // Navigation 상태 가져오기

  return (
    <div>
      {navigation.state <=== "idle" && <p>앱이 대기 중입니다.</p>}
      {navigation.state === "loading" && <p>로딩 중입니다...</p>}
      {navigation.state === "submitting" && <p>데이터를 제출 중입니다...</p>}
    </div>
  );
}

📌 에러 처리

loader에서 에러가 나더라도 위의 errorElement를 찾을 때까지 올라오다가 상위에서 errorElement를 만나면 그 컴포넌트가 실행된다.

<function App() {
  const router = createBrowserRouter([
    {
      path: "/",
      element: <RootLayout />,
      errorElement: <ErrorPage />,
      children: [
        { index: true, element: <HomePage /> },
        {
          path: "events",
          element: <EventLayout />,
          children: [
            {
              index: true,
              element: <EventsPage />,
              // errorElement: <ErrorPage />, // 만약 여기에 에러페이지 설정했다면
              // loader에서 에러 발생 시 여기 컴포넌트 실행
              // 하지만 여기에 설정안하고 상위에서 설정했다면
              // errorElement를 찾을 때까지 거슬러 올라가다가 만나면 그 에러 컴포넌트 실행
              loader: eventsLoader,
            },
            { path: ":eventId", element: <EventDetailPage /> },
            { path: "new", element: <NewEventPage /> },
            { path: ":eventId/edit", element: <EditEventPage /> },
          ],
        },
      ],
    },
  ]);

🎯 useRouterError

useRouteError 훅은 라우팅 과정에서 발생한 에러를 처리할 수 있도록 해준다. 이를 통해 특정 경로에서 로드한 데이터가 실패하거나 예상치 못한 오류가 발생했을 때 사용자에게 유용한 피드백을 제공할 수 있다.

1. loader 함수에서 에러 발생

loader 함수는 데이터를 가져오기 전에 실행되므로, 네트워크 요청 실패나 예외 상황에서 throw를 통해 에러를 발생시킬 수 있다.

❗️ 에러는 JSON 데이터를 포함한 Response 객체로 만들어야 하며, 상태 코드 status와 메시지를 명시할 수 있다.

📁 EventsPage.js

export async function loader() {
  const response = await fetch("http://localhost:8080/events");

  if (!response.ok) {
    throw new Response(JSON.stringify({ message: "Could not fetch event" }), {
      status: 500,
    });
  } else {
    const resData = await response.json();
    return resData.events;
  }
}

2. 에러 처리 컴포넌트

useRouteError 훅을 사용해 라우터가 제공하는 여러 객체를 가져온다.
에러 객체는 아래의 정보를 포함한다.

  • status: HTTP 상태 코드
  • data : throw로 전달한 JSON 데이터 (이 코드에서는 message)
📁 ErrorPage.js

import { useRouteError } from "react-router-dom";

import PageContent from "../components/PageContent";

export default function ErrorPage() {
  const error = useRouteError();

  error.status // 위에서 설정한 대로 에러난 경우 : 500
  
  let title = "An error occurred!";
  let message = "Something went wrong!";

  if (error.status === 500) {
    message = JSON.parse(error.data).message; // JSON 객체를 문자열로 전환
  }

  if (error.status === 404) {
    title = "Not Found!";
    message = "Could not find resource or page";
  }

  return (
    <PageContent title={title}>
      <p>{message}</p>
    </PageContent>
  );
}

loader

useParams 처럼 훅은 loader 함수에서 접근할 수 없다.

useRouteLoaderData

📁 App.js

{
  path: ":eventId",
  element: <EventDetailPage />,
  loader: eventDetailLoader,
},
{ path: ":eventId/edit", element: <EditEventPage /> },

위의 코드는 라우트를 중첩하기 이전의 코드이다.

이 코드에서 라우트를 중첩 시키려면 아래와 같이 작성하면 된다.
하지만 이렇게 중첩 라우트를 사용하면 loader 함수는 가장 가까운 컴포넌트에서만 사용되므로 EventDetailPage 에서는 데이터를 사용할 수 있지만, EditEventPage에서는 데이터를 사용할 수 없는 문제점이 생긴다.

📁 App.js
{
  path: ":eventId",
  // 공통 element 없으면 안적어도 된다.
  loader: eventDetailLoader, 
  children: [
      {
       index: true,
       element: <EventDetailPage />,
      },
      { path: ":eventId/edit", element: <EditEventPage /> },
  ],
},

따라서 아래의 코드와 같이 라우터에 id를 원하는 이름으로 지정해주고, 각 컴포넌트에서 원하는 데이터를 꺼내어 쓸 때 useRouteLoaderData('id') 를 사용한다.

이렇게 하면 중첩된 라우트의 하위에서도 데이터를 원하는대로 꺼내쓸 수 있다.

📁 App.js
{
   path: ":eventId",
⭐️ id: "event-detail", ⭐️
   loader: eventDetailLoader,
   children: [
        {
           index: true,
           element: <EventDetailPage />,
        },
        { path: ":eventId/edit", element: <EditEventPage /> },
    ],
},

📁 EditEventPage.js
  
import { useRouteLoaderData } from 'react-router-dom';
const data = useRouteLoaderData("event-detail");
const event = data.event;
  
📁 EventDetailPage.js
import { useRouteLoaderData } from "react-router-dom";
  
const data = useRouteLoaderData("event-detail");

0개의 댓글