해당 내용은 react router v5의 내용을 담고 있습니다.
만약 사용자가 개발자가 의도하지 않은 경로가 이동하려고 한다면 어떻게 해야할까요??
리액트 라우터에서 개발의도를 벗어나는 페이지로 이동하려고 할때, 해당 앱은 종료되지도 뭔가 나오지도 않는 애매한 상태가 놓이게 됩니다.
해당 문제는 switch를 통해 처리가 가능합니다. 의도하지 않은 경로의 경우 우선순위 마지막 "*" 를 추가하여 에러 페이지를 렌더링 하는 것입니다.
// NotFound 컴포넌트 렌더링
function App() {
return (
<Layout>
<Switch>
<Route path="/" exact>
<Redirect to="/quotes" />
</Route>
<Route path="/quotes" exact>
<AllQuotes />
</Route>
<Route path="/quotes/:quoteId">
<QuoteDetail />
</Route>
<Route path="/new-quote">
<NewQuotes />
</Route>
<Route path="*">
<NotFound />
</Route>
</Switch>
</Layout>
);
}
export default App;
예를들어, 이벤트 후 새로운 페이지로 이동해야하는 경우에는 어떻게 처리를 해야할까요??
우리가 흔히 썼던 게시판 사이트를 생각해봅시다. 한 사용자가 게시글을 정상적으로 올리는 경우, 알아서 조회 페이지로 이동하게 되잖아요
이러한 경우 사용하면 좋은 기능이 바로 useHistory() 입니다. useHistory() 는 브라우저의 history 객체에 접근이 가능합니다.
이와 비슷한 기능으로 useLocation()이 있습니다. useLocation() 역시, 브라우저의 location 객체에 접근하는 것이 가능합니다.
useHistory() 는 버전 업 이후 useNavigate()로 변경되었다고 합니다.
const QuoteList = (props) => {
const history = useHistory();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const isSortingAscending = queryParams.get("sort") === "asc";
const sortedQuotes = sortQuotes(props.quotes, isSortingAscending);
const changeSortingHandler = () => {
history.push({
pathname: location.pathname,
search: `?sort=${isSortingAscending ? "desc" : "asc"}`,
});
};
return (
<Fragment>
<div className={classes.sorting}>
<button onClick={changeSortingHandler}>
Sort {isSortingAscending ? "Descending" : "Ascending"}
</button>
</div>
<ul className={classes.list}>
{sortedQuotes.map((quote) => (
<QuoteItem key={quote.id} id={quote.id} author={quote.author} text={quote.text} />
))}
</ul>
</Fragment>
);
};
export default QuoteList;
앱을 사용할 때 페이지 이동을 방지해야 하는 경우가 있습니다. 다시 게시판을 예로 들자면, 유저가 게시글을 쓰다가, 해당 페이지에서 나가는 경우, 게시글을 새로 써야하는 단점이 발생합니다.
따라서 사용자가 현재 페이지를 이동하면 안되는 상황에 있다는 걸 확인하면 페이지 이동을 방지하는 이벤트를 걸어주면 좋지 않을까요?? 이를 Prompt를 사용하여 생성해봅시다.
Prompt는 해당 자식 컴포넌트를 감싸는 래퍼 컴포넌트입니다. when을 통해 조건 상태를 확인하며, 조건이 true인 경우 Prompt의 기능이 발현됩니다. message 속성은 경로 전환 방지 시, 값을 내용으로 출력해줍니다.
const QuoteForm = (props) => {
const authorInputRef = useRef();
const textInputRef = useRef();
const [isEntering, setIsEntering] = useState(false);
function submitFormHandler(event) {
event.preventDefault();
const enteredAuthor = authorInputRef.current.value;
const enteredText = textInputRef.current.value;
// optional: Could validate here
props.onAddQuote({ author: enteredAuthor, text: enteredText });
}
const formFocusedHandler = () => {
setIsEntering(true);
};
const finishEnteringHandler = () => {
setIsEntering(false);
};
return (
<>
<Prompt
when={isEntering}
message={(location) =>
"Are you sure you want to leave? All your entered data will be lost!"
}
/>
<Card>
<form onFocus={formFocusedHandler} className={classes.form} onSubmit={submitFormHandler}>
{props.isLoading && (
<div className={classes.loading}>
<LoadingSpinner />
</div>
)}
<div className={classes.control}>
<label htmlFor="author">Author</label>
<input type="text" id="author" ref={authorInputRef} />
</div>
<div className={classes.control}>
<label htmlFor="text">Text</label>
<textarea id="text" rows="5" ref={textInputRef}></textarea>
</div>
<div className={classes.actions}>
<button onClick={finishEnteringHandler} className="btn">
Add Quote
</button>
</div>
</form>
</Card>
</>
);
};
export default QuoteForm;
쿼리의 경우 파라미터와는 다르게 react 측에서 무언가를 설정해야할 필요는 없습니다.
파라미터는 switch 와 Route를 사용했었죠!
해당 경로의 쿼리 값을 읽어오기 위해, 이전에 썼던 useLocation을 활용해봅시다.
const QuoteList = (props) => {
const history = useHistory();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const isSortingAscending = queryParams.get("sort") === "asc";
const sortedQuotes = sortQuotes(props.quotes, isSortingAscending);
const changeSortingHandler = () => {
history.push({
pathname: location.pathname,
search: `?sort=${isSortingAscending ? "desc" : "asc"}`,
});
};
return (
<Fragment>
<div className={classes.sorting}>
<button onClick={changeSortingHandler}>
Sort {isSortingAscending ? "Descending" : "Ascending"}
</button>
</div>
<ul className={classes.list}>
{sortedQuotes.map((quote) => (
<QuoteItem key={quote.id} id={quote.id} author={quote.author} text={quote.text} />
))}
</ul>
</Fragment>
);
};
export default QuoteList;
일단 이전에 세팅한 switch 내의 기본 경로 구성을 다시 봅시다.
function App() {
return (
<Layout>
<Switch>
<Route path="/" exact>
<Redirect to="/quotes" />
</Route>
<Route path="/quotes" exact>
<AllQuotes />
</Route>
<Route path="/quotes/:quoteId">
<QuoteDetail />
</Route>
<Route path="/new-quote">
<NewQuotes />
</Route>
<Route path="*">
<NotFound />
</Route>
</Switch>
</Layout>
);
}
export default App;
만약 여기의 기본경로를 바꾸는 경우 해당 컴포넌트 내부의 자식 컴포넌트의 <Link>
의 경로 혹은 <Route>
경로 역시 모두 바꿔줘야 합니다. 앞전의 경로를 그대로 이어오는 동적 경로 기능을 통해 이러한 반복적인 수정작업을 회피할 수 있습니다.
useRouterMatch() 는 match 객체에 접근하게 됩니다. match 객체는 지나온 url 이나, path 값을 가져와줍니다. 이를 통해 좀 더 유연한 경로로 만들어 줍시다.
const QuoteDetail = () => {
const match = useRouteMatch();
const params = useParams();
const { sendRequest, status, data: loadedQuote, error } = useHttp(getSingleQuote, true);
const { quoteId } = params;
useEffect(() => {
sendRequest(quoteId);
}, [sendRequest, quoteId]);
if (status === "pending") {
return (
<div className="centered">
<LoadingSpinner />
</div>
);
}
if (error) {
return <p className="centered">{error}</p>;
}
if (!loadedQuote.text) {
return <p>No quote found!</p>;
}
return (
<>
<HighlightedQuote text={loadedQuote.text} author={loadedQuote.author} />
<Route path={match.path} exact>
<div className="centered">
<Link className="btn--flat" to={`${match.url}/comments`}>
Load Comment
</Link>
</div>
</Route>
<Route path={`${match.path}/comments`}>
<Comment></Comment>
</Route>
</>
);
};
export default QuoteDetail;