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 라이브러리를 이용하면 된다. 그렇지 않으면 조건부 렌더링을 하면 되지만 번거로우니 라이브러리를 사용하자.
Using React-Router
무슨 버전이든 상관 없다.
npm install react-router-dom@5
5버전을 설치한다.import { Route } from 'react-router-dom
Route를 import 한다.- Route 컴포넌트를 부르고 url을 path prop에 설정한 후 렌더링하고 싶은 컴포넌트를 넣는다.
// App.js <Route path="/welcome"> <Welcome /> </Route>
- 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"}`,
});
하드코딩 줄이기
중첩 라우트를 사용하다 보면 주소를 하드 코딩해야 하는데 이는 만약 주소가 변경되는 경우 바뀌는 과정에서 문제가 생길 수 있다. useLocation
과 useRouteMatch
를 사용하여 하드코딩을 줄일 수 있다.
- match.path : 정의된 라우트 ex) /quotes/:quoteId
- match.url : 현재 라우트
- location.pathname : 현재 라우트
react-router v6
-
참고로 중첩 route는 Routes로 감싸야 하며 부모 Route에서 끝에 “/*“를 추가해야 한다. 그리고 자식 Route에서는 부모 경로에 상대적이다.
Switch
→Routes
중첩 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="/" />
useHistory
→useNavigate
, 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 완벽 가이드
댓글남기기