ET네 만물상 - GitHub Repository / 배포 링크
const routes: RouteSetType[] = [
["/", MainPage, true],
["/welcome", WelcomePage, true],
["/login", LoginPage, true],
["/signup", SignupPage],
["/category", CategoryPage],
["/order", OrderPage, true],
["/result", OrderResultPage],
["/detail", DetailPage],
["/cart", CartPage],
["/mypage", MyPage],
["/search", SearchPage],
["/admin", AdminPage],
["/404", NotFound, true],
];
const App = () => {
return (
<Router>
{routes.map(([path, component, exact]: RouteSetType) => (
<Route
path={path}
exact={exact ?? false}
key={path}
component={component}
/>
))}
</Router>
);
};
const checkPath = (
targetPath: string,
currPath: string,
exact: boolean
): boolean => {
if (exact) {
return targetPath === currPath;
} else {
return currPath.match(new RegExp(targetPath, "i"))?.index === 0;
}
};
export const moveTo = (path: string) => {
const routeEvent = new CustomEvent("pushstate", {
detail: {
pathname: path,
},
});
window.dispatchEvent(routeEvent);
};
moveTo는 사용자 이벤트 없이 코드 내에서 path를 변경시키고 싶을 때 사용한다.
이벤트 객체(e)의 detail.pathname
속성에 입력받은 path 값을 넣어서 pushstate
이벤트를 발생시킨다
이 이벤트는 후술할 Router
가 window에 등록한 리스너가 감지해서 동작할 것이다!
export const decodeParams = (
encoded = window.location.search
): URIParameterType | null => {
const params = {};
const query = encoded.substring(1);
const vars = query.split("&");
vars.forEach((v) => {
const pair = v.split("=");
const key = decodeURIComponent(pair[0]);
const value = pair[1] ? decodeURIComponent(pair[1]) : null;
params[key] = value;
});
return params;
};
url의 query부분에 있는 key-value 값들을 추출하는 함수이다.
window.location.search
는 현재 url의 쿼리 부분, 즉? 뒤를 가져온다. encoded가 입력 받는게 아니라면 현재 url을 기본으로 한다.
substring(1)
로 ?를 제거하고 / split("&")
로 각 쿼리들을 얻은 후 반복을 돌리며 key, value를 추출해서 params 객체에 담는다.
export const Router = ({ children }): ReactElement => {
const setLocation = useSetRecoilState(locationState);
const isLoggedIn = useRecoilValue(loginState);
const setSelectedCategoryState = useSetRecoilState(selectedCategoryState);
const locationState = atom<LocaitionStateType>({
key: "location",
default: {
location: window.location.pathname,
params: decodeParams(),
},
});
const loginState = atom({
key: "isLoggedin",
default: null,
});
const selectedCategoryState = atom({
key: "selectedCategory",
default: {
categoryId: initParams.category ? parseInt(initParams.category) : 0,
subCategoryId: initParams.subCategory
? parseInt(initParams.subCategory)
: null,
},
});
먼저 위에서 본 것 처럼 Router
는 routes
배열의 각 요소를 props로 전달 받은 Route
컴포넌트들을 children으로 받는다.
recoil을 사용해서 전역으로 관리되고 있는 현재 로그인 상태 / 현재 location / 선택된 카테고리. 3가지 상태를 받아온다.
Router는 총 6개의 지역함수를 가지고 있다.
const checkPathValidation = () => {
const exist = routes.find(([path, component, exact]: RouteSetType) =>
checkPath(path, window.location.pathname, exact)
);
!exist && moveTo(NOT_FOUND);
};
const addEvents = () => {
window.addEventListener("pushstate", handlePushState);
window.addEventListener("popstate", handlePopState);
};
pushstate
, 'popstate이벤트 리스너를 등록한다.
Router의
useEffect`의 콜백에서 실행 될 것const setCurrentCategory = () => {
const params = decodeParams();
setSelectedCategoryState({
categoryId: params.category ? parseInt(params.category) : -1,
subCategoryId: params.subCategory && parseInt(params.subCategory),
});
};
이 함수는 사실 커스텀 라우터의 기능이라기보다, 프로젝트에서 사용하는 카테고리와 관련한 커스텀 커스텀 기능이라고 볼 수 있다. 한 마디로 우리 프로젝트에서 사용하기 때문에 필요한 것..
현재 url의 query를 추출해서 카테고리 id와 서브카테고리 id 상대를 업데이트한다
const setCurrentLocation = () => {
setLocation({
location: window.location.pathname,
params: decodeParams(),
});
document.documentElement.scrollTo(0, 0);
setCurrentCategory();
};
const handlePushState = (e: HistoryEvent) => {
const path = e.detail.pathname;
window.history.pushState({}, "", path);
setCurrentLocation();
};
const handlePopState = (e) => {
setCurrentLocation();
};
pushstate
, popstate
이벤트 리스너에 달린 핸들러.
pushstate
는 이벤트 발생시 전달받은 detail의 pathname을 받아서 history에 저장하고 setCurrentLocation를 호출한다
export const Route = ({ exact, path, component: Component }: RouterType) => {
const { location } = useRecoilValue(locationState);
return checkPath(path, location, exact) ? <Component /> : null;
};
Router
에서 routes 배열을 돌렸을 때 해당하는 Route만 남고 나머지가 null이 되는 것export const Link = ({
to,
children,
}: {
to: string;
children: React.ReactChild | React.ReactChild[];
}) => {
const handleClickLink = () => {
moveTo(to);
};
return <LinkWrapper onClick={handleClickLink}>{children}</LinkWrapper>;
};
const LinkWrapper = styled.a`
cursor: pointer;
`;
Router
컴포넌트의 자식으로 routes.map을 통해 각 요소를 Route
에 전달한다.Route
는 현재 url과 일치하는 경우에만 해당 component를 return한다pushstate
이벤트가 발생하면 현재 pathname을 history에 저장하고 전달받은 pathname을 loaction에 추가한다 (이동)기본 흐름은 이러하고, 디테일한 내용은 각 함수 설명글을 참고하자..
라이브러리를 사용하지 않고 직접 구현한 것은 라이브러리에서 제공하지 않는 기능이나 결함이 있어서 인가요?! 아니면 학습을 위해서?? 궁금하네요 ^0^