
새 게시글을 작성하거나 게시글을 수정할 때 ui나 url로 해당 경로에 이동하면 로그인 상태가 아니거나 만료된 토큰일 때 로그인 페이지로 이동시켜야 한다. 그럼 이전에 작성 중인 데이터가 날라가는데 이걸 막기 위해 navigate와 location를 사용해보기.!
https://reactrouter.com/en/main/hooks/use-location
이 훅은 location 개체를 반환
import * as React from 'react';
import { useLocation } from 'react-router-dom';
function App() {
let location = useLocation();
// https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
location.hash: `#` 현재 페이지의 hash. 없는 경우 `''`
location.key: 고유 식별키 (어떤 경우에 사용하는지 아직 모르겠음)
location.pathname: 현재 페이지의 경로 URL
location.search: `?` 다음 쿼리 문자열. 없는 경우 `''`
location.state: url에 넣고 싶지 않은 데이터를 저장하는 장소
}
https://reactrouter.com/en/main/hooks/use-navigate
import { useNavigate } from "react-router-dom";
function App() {
const navigate = useNavigate();
navigate('/이동할 경로');
// or state등의 options 전달
navigate(to, { state: any });
// 이전 주소로 이동
navigate(-1);
}
register 페이지로 이동해서 로그인을 해야 하는 경우, navigate의 두 번째 인자인 options의 state에 현재 path와 보관해야 할 data 값이 담긴 객체를 전달했다.
navigate('/register', { state: { path: '', data: {} }});
그리고 로그인 컴포넌트에 goToPrevOrHome이라는 로그인 완료 후에 이전 페이지나 홈 페이지로 이동할 수 있는 함수를 만들어서 props로 내려줌
pages/Register.tsx
export default function Register() {
const location = useLocation();
// path: 이전 경로, data: 이전 데이터
const locationState = location.state as { path: string; data: any };
const navigate = useNavigate();
const { value: isRegistered, setValue: setIsRegistered } = useInput(true);
const goToHome = () => navigate('/');
// - register 이전 경로로 이동해야 하는 경우 이동 경로를 locationStat.path를
// 없으면 home(/) 으로
// - 전달해야 할 data가 있는 경우 locationState.data를 없으면 null
const goToPrevOrHome = () =>
navigate(locationState ? locationState.path : '/', {
state: locationState ? { data: locationState.data } : null,
});
return (
<div>
{!isRegistered ? (
<SignUp goToHome={goToHome} setIsRegistered={setIsRegistered} />
) : (
<SignIn
goToPrevOrHome={goToPrevOrHome}
setIsRegistered={setIsRegistered}
/>
)}
</div>
);
}
components/register/SignIn.tsx
function SignIn({ goToPrevOrHome, setIsRegistered }: SignInProps) {
...
const handleSignIn = async (
event:
| React.FormEvent<HTMLFormElement>
| React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault();
loginRequest({ username: id.value, password: pw.value })
.then((data) => {
const errorCode = data.errorCode;
const errorMessage = data.message;
if (errorCode) {
...
} else {
setCurrentUser(data);
// 로그인 완료 시
goToPrevOrHome();
}
})
.catch((err) => {
console.log('login err', err);
});
};
return (...);
}
/board/new 페이지 접속 시
게시글을 등록할 때 만료된 토큰이면
export default function WritePost() {
...
const handleCreateBoard = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const instance = editorRef.current?.getInstance();
const category = formData.get('category') as string;
const title = formData.get('title') as string;
const content = instance?.getHTML() as string;
...
const data = {
category,
title,
imgList,
content,
deletedImgList,
};
createPostRequest(currentUser?.accessToken as string, data)
.then((res) => {
if (!res.errorCode) {
navigate(`/board/${res.boardId}`);
} else {
// 만료된 토큰일 때 (세부로직작성하기)
alert(res.message);
resetUser();
// register 페이지로 현재 경로와 작성 중인 데이터를 보냄
navigate('/register', {
state: { path: '/board/new', data },
});
}
})
.catch((err) => {
console.log('createBoardRequest err', err);
});
}
...
}
로그인하고 돌아와서 기존 데이터 받기
register page로 전달된 location 데이터들로 만든 goToPrevOrHome 함수로 로그인 후에 다시 이전 페이지로 이동하는데, 만약 토큰 문제없이 처음에 글을 쓰는 경우에는 location state 정보가 없으므로 || 연산자로 빈 문자열을 작성해 각 input의 default or initial value로 설정해 줬다.
interface LocationState {
data: {
category: string;
title: string;
content: string;
contentImgList: string[];
tempImgList: string[];
};
}
export default function WritePost() {
const navigate = useNavigate();
const location = useLocation();
const locationState = location.state as LocationState;
let contentImgList: string[] = locationState?.data.contentImgList || [];
let tempImgList: string[] = locationState?.data.tempImgList || [];
...
return (
<form onSubmit={handleCreateBoard}>
<div>
<select
defaultValue={locationState?.data.category || ''}
>
...
</select>
<input
defaultValue={locationState?.data.title || ''}
...
/>
</div>
<ToastEditor
initialValue={locationState?.data.content || ''}
...
/>
<div>
<button>취소</button>
<button type="submit">등록</button>
</div>
</form>
);
}
게시글 작성 페이지 이동 버튼
WritePost component 내부에 작성해도 되지만 어차피 location을 받으니까 게시글 작성 버튼을 눌러서 이동할 때도 state data를 보냈다ㅏ 근데 url로 접속할 때도 처리해야 하니까 한 번 더 생각해봐야겠드
<button
onClick={() =>
navigate('/board/new', {
state: {
data: {
category: '',
title: '',
content: '',
contentImgList: [],
tempImgList: [],
},
},
})
}
>
글쓰기
</button>
수정하기 버튼을 눌러서 edit 페이지로 이동할 때와, url로 이동할 때, 토큰 문제로 로그인 페이지를 갔다 돌아올 때 등 게시글 작성할 때와 마찬가지로 작성
수정 버튼
게시글에서 수정하기를 누른 경우 해당 게시글의 id, title 등의 정보를 보냄
export default function Post() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const { data } = useQuery(`BoardDetail${id}`, () =>
getPostRequest(Number(id))
);
const handleEditBoard = () => {
navigate(`/board/${id}/edit`, {
state: {
data: {
boardId: data?.boardId,
boardCategory: data?.boardCategory,
title: data?.title,
content: data?.content,
},
},
});
};
...
처음 페이지에 들어왔을 때 useEffect 내부에서 유저의 토큰이 존재하는지 확인하는데, 있으면 boardId에 해당하는 수정 데이터를 가져오고 없으면 이전 경로로 navigate. 그리고 게시글 수정을 요청했을 때 토큰이 만료됐으면 이전 경로와 formData를 state로 보내기!
pages/EditPost.tsx
import { useLocation, useNavigate } from 'react-router-dom';
import { Img } from '../modules/board/type';
interface LocationState {
data: {
boardId: number;
title: string;
boardCategory: string;
content: string;
contentImgList: Img[];
tempImgList: Img[];
};
}
export default function EditPost() {
const location = useLocation();
const navigate = useNavigate();
const locationState = location.state as LocationState;
const boardId = Number(location.pathname.split('/')[2]);
let tempImgList: Img[] = locationState?.data?.tempImgList || [];
let contentImgList: Img[] = locationState?.data?.contentImgList || [];
const editorRef = useRef<ToastEditor>(null);
const currentUser = useRecoilValue(currentUserState);
const resetUser = useResetRecoilState(currentUserState);
useEffect(() => {
if (currentUser?.accessToken) {
getPrevPostDataRequest(
currentUser?.accessToken as string,
boardId // || editData.boardId
)
.then((res) => {
console.log('getPrevBoardDataRequest res', res);
if (res.error) return navigate(-1);
if (res.errorCode === 'EXPIRE_ACCESS_TOKEN') {
alert(res.message);
navigate('/register', {
state: { prevPath: location.pathname },
});
return;
}
// setEditData(res);
contentImgList = res.imgList;
tempImgList = res.imgList;
editorRef.current?.getInstance().setHTML(res.content);
})
.catch((err) => {
console.log('getPrevBoardDataRequest err', err);
});
} else {
alert('수정 권한이 없습니다');
navigate(-1);
}
}, []);
const handleCreateBoard = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// const data = Object.fromEntries(formData);
const instance = editorRef.current?.getInstance();
const content = instance?.getHTML() as string;
const boardCategory = formData.get('boardCategory');
const title = formData.get('title');
...
const data = {
boardCategory,
title,
boardId,
content,
boardImg,
deleteImg,
};
updatePostRequest(currentUser?.accessToken as string, data)
.then((res) => {
console.log('updateBoardRequest res', res);
const errorCode = res.errorCode || '';
if (!errorCode) {
navigate(`/board/${res.boardId}`);
} else {
alert(res.message);
if (errorCode === 'EXPIRE_ACCESS_TOKEN') {
resetUser(); // 토큰 만료
navigate('/register', {
state: { path: location.pathname, data },
});
}
// else if (errorCode === '') {}
}
})
.catch((err) => {
console.log('updateBoardRequest err', err);
});
};
return (
<form onSubmit={handleCreateBoard} className="">
<div>
<select
name="boardCategory"
defaultValue={locationState?.data?.boardCategory || ''}
>
<option value="">카테고리 선택</option>
<option value="VEGAN">비건</option>
<option value="ENVIRONMENT">환경</option>
<option value="QUESTION">Q&A</option>
<option value="FREE">자유게시판</option>
</select>
<input
defaultValue={locationState?.data?.title || ''}
...
/>
</div>
<ToastEditor
initialValue={locationState?.data?.content || ''}
...
/>
<div className="text-end mt-4">
<button>취소</button>
<button type="submit">등록</button>
</div>
</form>
);
}
오늘 해본 건 useNavigate, useLocation 훅으로 router 간? 데이터를 보내고 받아 작성하던 게시글 데이터가 남아있게 만들었다. 이전에 react-router-dom v6 이전 버전에서 history.push 했을 때도 state는 몇 번 사용해 봤는데 별거 아니지만 다른 페이지에 갔다 왔는데 이전에 존재하던 페이지로 이동하고 작성 중이던 데이터가 그대로 보이니 감회가 새로웠따
근데 이건 어딘가에 저장돼있는 상태가 아니라 새로고침하거나 나가면 없어지니까 로컬스토리지를 사용해서 다시 구현해보고 싶다. 그리고 더 좋은 방법이 있는지랑 useEffect를 이용하지 않고 토큰 존재 여부 등의 로직을 처리할 수 있는 방법이 있는지 찾아봐야지