SPA Routing

학습 목표

  • What is Client-Side “Routing”?
  • Using React-Router
  • Advanced Features: Dynamic & Nested Routes

Single-Page Application Routing: Multiple Pages In Single-Page Apps

What is Client-Side “Routing”?

기존에는 HTML 파일을 요청해야 하고 응답을 기다려야 했다. 그러나 이 방식은 원치 않으니 우리가 싱글 페이지 애플리케이션으로 온 것이 아닌가? 그렇기에 우리는 라이브러리를 이용해서 url이 바뀌면 요청을 다시 보내는 것이 아닌 컴포넌트를 다시 불러오는 그런 동작을 구현할 수 있다. 즉, 조건부 컴포넌트 렌더링이라고 생각하면 된다. react router 라이브러리를 이용하면 된다. 그렇지 않으면 조건부 렌더링을 하면 되지만 번거로우니 라이브러리를 사용하자.

react router 공식 사이트

Using React-Router

무슨 버전이든 상관 없다.

  1. npm install react-router-dom@5 5버전을 설치한다.
  2. import { Route } from 'react-router-dom Route를 import 한다.
  3. Route 컴포넌트를 부르고 url을 path prop에 설정한 후 렌더링하고 싶은 컴포넌트를 넣는다.
    // App.js
    <Route path="/welcome">
      <Welcome />
    </Route>
    
  4. BrowserRouter 컴포넌트로 App 컴포넌트를 맵핑해줘야 한다.
    <BrowserRouter>
      <App />
    </BrowserRouter>
    

페이지 컴포넌트는 pages 폴더에 넣는 것을 추천한다.

Client Side Routing

링크를 누르면 페이지가 변경되는 것을 구현하고 싶을 것이다. 이를 위해서 Link 컴포넌트를 사용할 수 있다. 만약 a 태그를 사용하면 HTML 파일 요청을 보내기에 새로고침이 되고 데이터를 잃는 문제가 생긴다.

<Link to="/welcome">Welcome</Link>

만약 활성화 되었을 때 링크를 강조 표시하려면 NavLink의 activeClassName을 사용하면 된다.

<NavLink activeClassName={classes.active} to="/welcome" />

Advanced Features: Dynamic & Nested Routes

Dynamic Routes

만약 세부 사항을 보고 싶을 때 아이템 별로 링크를 추가하고 싶을 것이다. 그럴 때 세그먼트를 사용할 수 있다. 여러개 추가할 수 있다. 그리고 사용할 때는 react-router에서 만든 커스텀 훅인 useParams를 이용하면 된다. 사용할 때는 세그먼트에 사용한 이름으로 사용해야 한다.

// App.js
// 동적 세그먼트
return (
  <Route path="/products/:productId">
    <ProductDetail />
  </Route>
);

// ProductDetail.js
// useParams 사용
import { useParams } from "react-router-dom";

const ProductDetail = () => {
  const params = useParams();

  return <p>{params.productId}</p>;
};

이 때 이전 페이지의 이름을 그대로 이어서 추가하는 형식으로 하는 것이 사용자에게 직관적이므로 추천한다.

이렇게 사용하면 당연히 Products 페이지가 보일 것이고 그 밑에 detail 페이지가 출력될 것이다. 이는 이전 링크 뒤에 추가했기 때문인데 만약 detail만 보여주고 싶다면 Switch를 사용하면된다. 이렇게 사용하면 구체적인지 여부에 상관 없이 작동하기에 변함이 없다. 이럴 땐 두 가지 방법을 사용할 수 있다. 먼저 순서를 바꿔서 구체적인 링크를 먼저 체크하게 하거나 exact prop을 사용하여 정확히 똑같은 경우에만 보이도록 하는 방법이 있다.

// exact을 사용한 방법
<Switch>
  <Route path="/products" exact>
    <Products />
  </Route>
  <Route path="/products/:productId">
    <ProductDetail />
  </Route>
</Switch>

Nested Routes

페이지 내부에서도 Route를 사용하고 싶은 경우가 있을 것이다. 그럴 때 사용된다. 사용법은 똑같다.

// Welcome.js
<Route path="/welcome/new-user">
  <p>Welcome, new user!</p>
</Route>

Redirect Routes

만약 ‘/’ 주소에 도착했을 때 ‘/welcome’으로 redirect 되기를 원한다면 그 기능을 사용할 수 있다.

<Route path="/" exact>
  <Redirect to="/welcome" />
</Route>

Not Found Route

앱을 중단시키지 않으려면 찾을 수 없음 페이지를 만드는 것이 좋을 것이다. 방법은 간단하다. Switch 컴포넌트의 맨 마지막에 모든 경로를 의미하는 ‘*’ 경로로 Route 해주면된다. 순서가 중요함을 잊지 말자. 이렇게 하면 위에 작성한 경로를 제외하고 나머지 경로만 NotFound 페이지로 이동하게 된다.

<Route path="*">
  <NotFound />
</Route>

history

만약 제출하면 링크를 이동하고 싶다면 useHistory 훅을 사용하면 된다. 이 때 push는 뒤로 가기가 가능하고 replace는 뒤로 가기가 불가능하다는 차이점이 있다. 상황에 맞게 사용하면 된다.

const history = useHistory();

const handler = () => {
  history.push("/quotes"); // 뒤로 가기 가능
  history.replace("/quotes"); // 뒤로 가기 불가능
};

Prompt

만약 양식을 입력 중일 때 다른 링크로 가려고 하면 경고창을 띄워 줄 수 있다. when prop에 조건을 넣고 message에는 띄울 문자를 넣으면 된다. location 인자로 가려는 위치를 받을 수 있다.

참고로 onSubmit할 때 isEntering를 false로 하면 늦는다. 동기 처리기 때문에 버튼을 클릭 했을 때 상태를 변경해주는 것이 좋다.

<Prompt
  when={isEntering}
  message={(location) =>
    "Are you sure you want to leave? All your entered data will be lost!"
  }
/>
<Card />

query params

쿼리 매개변수를 사용하고 싶다면 useLocation 훅을 사용해주면 된다. location.search에서 쿼리를 찾을 수 있다. URLSearchParams 객체로 변해서 불러오고 sort 쿼리의 값을 가져온 뒤 bool 값 변수로 생성해 다른 곳들에 사용하면 된다.

const location = useLocation();

const queryParams = new URLSearchParams(location.search);
const isSortingAscending = queryParams.get("sort") === "asc";

const changeSortingHandler = () => {
  history.push("/quotes?sort=" + (isSortingAscending ? "desc" : "asc"));
};

복잡한 쿼리의 경우 이렇게 사용하여 가독성을 높일 수 있다.

history.push({
  pathname: location.pathname,
  search: `?sort=${isSortingAscending ? "desc" : "asc"}`,
});

하드코딩 줄이기

중첩 라우트를 사용하다 보면 주소를 하드 코딩해야 하는데 이는 만약 주소가 변경되는 경우 바뀌는 과정에서 문제가 생길 수 있다. useLocationuseRouteMatch를 사용하여 하드코딩을 줄일 수 있다.

  • match.path : 정의된 라우트 ex) /quotes/:quoteId
  • match.url : 현재 라우트
  • location.pathname : 현재 라우트

react-router v6

  • 참고로 중첩 route는 Routes로 감싸야 하며 부모 Route에서 끝에 “/*“를 추가해야 한다. 그리고 자식 Route에서는 부모 경로에 상대적이다.
    SwitchRoutes
    중첩 Route 사용 시 부모 Route<Route path="/welcome/*" element={<Welcome />} />
    자식 Route<Route path="hello" element={<Hello />} />

    • 이런 방식도 있지만 다른 방식으로 사용할 수 있다. 중첩 Route를 부모 Route가 있는 곳으로 감싸주는 것이다. 이는 모든 Route 정의를 한 곳에 모을 수 있다는 장점이 있다.

      <Route path="/welcome/*" element={<Welcome />}>
        <Route path="hello" element={<Hello />} />
      </Route>;
      
      // 컴포넌트에 중첩된 route를 어디에 넣을지 알려줌
      return (
        <div>
          <Outlet />
        </div>
      );
      
  • exact 사용 없이 항상 일치하는 경우만 체크한다. 자식 컴포넌트가 아닌 element에 넣기. 내부 로직이 똑똑해져서 순서 상관 없다. index 사용하면 부모 따라 같은 주소로 설정 가능하다.

    // v5
    <Route path="/:id" exact>
      <Welcome />
    </Route>
    

    // v6
    <Route path="/:id" element={<Welcome />} />
    
  • activeClassName prop 삭제되었으니 className이나 style로 직접 동적 스타일링하기, 여기도 마찬가지로 중첩인 경우 부모 경로에 상대적이다.
    <NavLink activeClassName={classes.active} to="/" /><NavLink className={(navData) => navData.isActive ? classes.active : ''} />
  • push일 때는 그냥 사용하면 되고 replace를 추가할 수 있다.
    <Redirect to="/" /><Navigate replace to="/" />
  • useHistoryuseNavigate, navigate는 숫자를 넣을 수 있다.
    • history.push("/welcome")navigate("/welcome")
    • history.replace("/welcome")navigate("/welcome", {replace : true})
  • <Prompt>는 아쉽지만 현재까진 지원하진 않는다.

v6.4

6.4 버전에서 데이터 Fetching, Submission을 간소화하는 기능이 생겼다.

Fetching

컴포넌트 안의 로딩, http 요청 함수, 에러 처리 상태를 다 지운다. useLoaderData를 이용하여 데이터를 가져오도록 한다.

const Welcome = () => {
  const postData = useLoaderData();

  return <Post title={postData.title} />;
};

// 하단에 요청 함수를 반환하는 loader 함수를 만든다.
export const loader = () => {
  return getPosts();
};

// 만약 params가 필요한 경우도 사용할 수 있다.
export const loader = ({ params }) => {
  const postId = params.id;
  return getPost(postId);
};

그런 다음 Route에서 설정해주면 끝이다. 핵심은 Routes로 감싸는 것이 아닌 Route로 감싸는 것이다.

import { loader as welcomeLoader } from "..."; // 별칭으로 부르기
import { loader as detailLoader } from "...";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<RootLayout />}>
      <Route index element={<Welcome />} loader={welcomeLoader} />
      <Route path=":id" element={<Detail />} loader={detailLoader}>
    </Route>
  )
);

그렇다면 오류는 어떻게 처리할까? Route에선 errorElement 속성을, 접근하기 위해서 컴포넌트에선 useRouteError를 사용하면 된다. 참고로 버블링이 되므로 부모에서 사용해도 된다.

// App.js
<Route path="/" element={<RootLayout />} errorElement={<ErrorPage />} />;

// 컴포넌트.js
const error = useRouteError();

return <p>{error.message}</p>;
Submission

에러, onSubmit 함수 등을 다 삭제한다. 제공하는 새로운 Form 컴포넌트로 바꿔준다. 참고로 preventDefault를 작성할 필요가 없다.

<Form method="post" action="/blog" />

useActionData훅과 Route의 action prop을 사용하면 된다. action에 넣을 함수는 역시나 동적 세그먼트에서 온 params와 fetch로 얻은 request를 인자로 가진다.

import { actionFun } from "...";

<Route action={actionFun} />;

action 함수에서 redirect 함수를 반환해 페이지를 이동시킬 수 있으며 에러가 발생시 에러를 반환해주면 useActionData 훅으로 컴포넌트에서 이용할 수 있다.

// 컴포넌트.js
const error = useActionData();

return <p>{error.message}</p>;

또한 제출중 상태를 알기 위해선 useNavigation 훅을 사용하면 된다. 그 중 state에 담겨져 있다.

  • navigation.state
    • idle - There is no navigation pending.
    • submitting - A route action is being called due to a form submission using POST, PUT, PATCH, or DELETE
    • loading - The loaders for the next routes are being called to render the next page

위와 같은 기능을 Remix도 한다. 이를 이용하면 React에 기반한 풀스택 앱을 구축할 수 있지만 react-router는 프론트엔드라는 점이 차이점이다.

데이터 로딩 연기

이것은 이미 가져온 데이터를 먼저 보여주고 오래 걸리는 건 나중에 렌더링하고 싶을 때 사용할 수 있다.

defer 유틸리티를 이용하면 resolve된 값 대신 프로미스를 전달해 loader에서 반환된 값을 연기할 수 있다.

const loader = async () => {
  let product = await getProduct(); // 대기하고 렌더하라는 뜻
  let reviews = getProductReviews(); // 대기 안해도 됨
  return defer({ product, reviews });
};

<Await /> 컴포넌트는 자동 오류 처리와 함께 지연된 값을 렌더링하는데 사용된다. resolve prop을 가지는데 이는 지연된 loader 값에서 반환된 프로미스를 resolve하고 렌더링한다. 그리고 errorElement prop을 가지는데 이는 프로미스가 reject할 때 자식 컴포넌트 대신 렌더링할 에러 element이다. 그리고 이 <Await /> 컴포넌트를 React의 Suspense 컴포넌트로 감싸줘야 한다. 그래야 이 컴포넌트가 렌더링하기 전에 다른 작업이 먼저 이루어지도록 대기한다. 그 다른 작업을 fallback prop으로 작성하면된다.

<Suspense fallback={<p>Loading...</p>}>
  <Await resolve={loaderData.posts}></Await>
</Suspense>
useFetcher hook

기본적으로 submit이나 loader를 수동으로 트리거할 때 사용한다. 이는 페이지 전환 없이 요청을 보내고 싶을 때 사용한다.

// fetcher.Form -> navigation 발생하지 않는 것을 제외하고는 <Form /> 컴포넌트와 같다.
const fetcher = useFetcher();
return (
  <fetcher.Form method="post" action="/some/route">
    <input type="text" />
  </fetcher.Form>
);
fetcher.submit(data, options); // 사용자의 상호 작용이 fetch를 시작하는 경우가 아닌 프로그래머가 fetch를 시작하는 경우에 사용한다.
fetcher.load(href); // route loader에서 데이터 로드

강의명

  • Udemy : React 완벽 가이드

카테고리:

업데이트:

댓글남기기