history stack과 push/replace/go
0. history에 대해서
https://reactrouter.com/web/api/history
react-router-dom을 사용하면서 별 생각 없이 history를 사용해왔는데 이번 프로젝트에서 조금 복잡한 라우팅 처리를하게 되면서 history에 대해서 알아할 필요를 느꼈다.
react-router-dom의 글을 대강 번역해보았다. 링크는 위에 뒀다.
히스토리, 히스토리 객체라는 용어는 react-router-dom의 의존 패키지인 the history package에 의거한 것입니다. 히스토리는 자바스크립트 환경에서의 히스토리 세션을 관리하는 도구입니다.
다음 용어도 사용되었습니다.
- “browser history” - A DOM-specific implementation, useful in web browsers that support the HTML5 history API
- “hash history” - A DOM-specific implementation for legacy web browsers
- “memory history” - An in-memory history implementation, useful in testing and non-DOM environments like React Native
히스토리 객체는 일반적으로 다음 속성과 메서드를 가집니다.
- length - (number) 히스토리 스택의 길이.
- action - (string) 현재 페이지에 오게 된 액션 (PUSH, REPLACE, or POP)
- location - (object) 현재 주소
- pathname - (string) The path of the URL
- search - (string) The URL query string
- hash - (string) The URL hash fragment
- state - (object) location-specific state that was provided to e.g. push(path, state) when this location was pushed onto the stack. Only available in browser and memory history.
- push(path, [state]) - (function) Pushes a new entry onto the history stack
- replace(path, [state]) - (function) Replaces the current entry on the history stack
- go(n) - (function) Moves the pointer in the history stack by n entries
- goBack() - (function) Equivalent to go(-1)
- goForward() - (function) Equivalent to go(1)
- block(prompt) - (function) Prevents navigation (see the history docs)
1. go/push/replace
history에 보면 go, goback, goForward, block, push, replace를 볼 수 있다.
goback과 goForward는 go(-1) go(1)로 각각 나타낼 수 있으므로 생략하고 결국 유저를 이동시키는 메서드로 go, push, replace가 남게 된다. (block은 팅겨내기이므로 따로 다루자.)
go는 단순히 히스토리의 어느 시점으로 이동하는 것이다. 당연히 새로고침을 해서 히스토리가 없는데 goBack을 때리면 히스토리가 없다는 오류를 낼 것이다.
유저를 이동시키는데에 주로 사용되는 건 push와 replace이다. 문제는 이 것들이 다 비슷비슷해보인다는 것이다. 차이를 알아보자.
히스토리는 stack으로 쌓인다. 여기에 push는 history 제일 위에 쌓는 것이고, replace는 history 제일 위에 있는 원소를 지금 넣을 원소로 바꾸는 것이다.
When you use the router.replace, you're overwritting the top of the the stack.
When using the router.push, it adds a new route to the top of the stack.
홈 -> 게시판 -> 로그인 순으로 움직였다고 가정하자.
로그인에서 push("/게시판")을 날리면 히스토리는 홈 -> 게시판 -> 로그인 -> 게시판이 된다.
반면 replace("/게시판)을 날리면 현재 히스토리인 로그인을 날려버리고 홈 -> 게시판 -> 게시판이 된다.
히스토리를 남기기 위해서 push를 사용하자. (사실 작성하다보면 대부분 push를 사용하게 된다.)
그렇다면 replace는 언제 사용해야 하는가? 잘못된 url이나 올바르지 못한 접근을 시도했을 때 강제로 redirect할 때 사용하는 것이 좋다. 예를 들어 로그인한 유저가 /login으로 접근하려고 할 때 돌려보내는 작업은 replace를 사용하는 것이 좋다. 그래야 잘못된 접근이 히스토리에 남지 않게 되기 때문이다.
아니면 접근을 아예 못하게 팅겨내버리거나...
🔥 tip! 로그인 처리에 있어서 경로 이동
(비 로그인 상태) 홈 -> 로그인 창 -> (로그인 상태) 홈
위와 같은 상태일 때 로그인을 한 후에 뒤로가기를 눌러 로그인 창으로 가면 어떻게 해야 하는가?
대부분 G마켓과 같은 홈페이지에서도 그냥 로그인창을 보여준다. 로그인만 안 끊기면 된다.
물론 HOC을 적용해 접근 금지를 시킨후 replace를 통해 홈으로 보내는 방법도 있다. 편한 쪽으로 선택하자.
그런데 만약 또 뒤로가기를 눌러서 홈으로 돌아가면?
로그인이 안 끊겼으니까 로그인 상태를 유지해주면 된다.
물론 매번 history의 action을 체크해서 block을 먹일 수도 있을 테지만, 수고에 비해 얻는 유저 경험에는 차이가 없어서 무익하다.
참고하면 좋은 글)
https://medium.com/w-bs-log/history-push%EC%99%80-replace%EC%9D%98-%EC%B0%A8%EC%9D%B4-ed5f2f7db7dc
2. push 시 데이터 전달하기 쥐여주기
로그인했는지를 체크해서 로그인이 되어 있지 않으면 강제로 홈으로 보내는 HOC을 하나 짰다.
문제는 유저가 왜 팅겼는지 알아야 한다는 것이다.
해결 방법을 찾아보면 중 push의 두번째 인자에 정보를 넘겨줄 수 있다는 사실을 알아냈다. (위 SO 답변 참고)
* 꽤 시간이 지난 후 포스트를 다시 읽어보니 Redux 등 전역 상태관리 툴을 이용해 넘겨주는 게 오히려 더 좋다고 생각이 든다.
import React, { useEffect, useState } from "react";
import axios from "axios";
function withAuthHoc(WrappedComponents) {
const AuthenticationCheck = (props) => {
const [userData, setuserData] = useState("");
useEffect(() => {
axios.post("/auth/jwtauthcheck").then((res) => {
setuserData(res.data);
if (!res.data.isAuth)
return props.history.push("/", {
data: "로그인한 회원만 업로드할 수 있습니다.",
});
});
}, []);
return <WrappedComponents user={userData} />;
};
return AuthenticationCheck;
}
export default withAuthHoc;
현재 / 로 push했는데 해당 경로에서 렌더 되는 컴포넌트의 props.location.state 항목에 전달한 정보를 참고할 수 있게 되었다. 정보가 없으면 undefined이므로 옵셔널 체이닝을 이용해 다음과 같이 작성할 수 있다.
import React, { useEffect } from "react";
import { withRouter } from "react-router-dom";
import { openNotification } from "../../utils";
function Home(props) {
useEffect(() => {
if (props.location.state?.data) {
openNotification(props.location.state.data);
props.history.replace("/", null);
}
}, []);
return (
<>
<div>Home!</div>
</>
);
}
export default withRouter(Home);
참고로, 처음에는 useEffect를 쓰지 않고 실행했는데 오류가 났다.
import React from "react";
import { withRouter } from "react-router-dom";
function Home(props) {
if (props.location.state?.data) {
alert(props.location.state.data);
}
return <div>Home!</div>;
}
export default withRouter(Home);
또, push로 이동한 후 새로고침을 해보면 해당 데이터가 그대로 남아 있어서 알림창이 또 뜨는 것을 볼 수 있는데 이는 history를 지워주면 해결된다.
(https://stackoverflow.com/questions/53278986/how-to-clear-props-location-state-on-refresh)
props.history.replace("", null);