1주차 과제는 생각보다 간단하고 재미있었다.
React > TypeScript 환경에서 History API를 사용해서 SPA Router를 구현하는 것이 과제의 목표였다.
실제로 React에서는 react-router-dom이라는 라이브러리를 사용해서 routing 처리를 한다.
이번 과제는 그래서 작은 custom react-router-dom을 구현한다고 생각했다.
1) 해당 주소로 진입했을 때 아래 주소에 맞는 페이지가 렌더링 되어야 한다.
/
→ root
페이지/about
→ about
페이지2) 버튼을 클릭하면 해당 페이지로, 뒤로 가기 버튼을 눌렀을 때 이전 페이지로 이동해야 한다.
window.onpopstate
, window.location.pathname
History API(pushState
)3) Router, Route 컴포넌트를 구현해야 하며, 형태는 아래와 같아야 한다.
ReactDOM.createRoot(container).render(
<Router>
<Route path="/" component={<Root />} />
<Route path="/about" component={<About />} />
</Router>
);
4) 최소한의 push 기능을 가진 useRouter Hook을 작성한다.
const { push } = useRouter();
Router or Route, 어디서 구현할지가 관심사였다...
Router
에 포함되는 모든 Route
가 일단 rendering 된다는 문제가 생긴다. Router
에서 필터링을 통해 rendering 시켜주자.간단하게 말하면
"git open source를 Repo를 파보자" 이다.
우리가 자주 사용하는 라이브러리, 프레임워크의 공식문서만 볼 것이 아니라 'code' 그 자체를 보는 것이 원리를 이해하고 구조를 파악하는데 꽤 도움이 된다는 것이다.
cf) 좋은 컴포넌트의 특징을 모방한다. (✅ google의 material ui code 파보기)
react-router git repo
코드를 읽는 능력은 더 연습해야한다.
// 구현해야하는 것 위주로 느낌만 보자
// Router
export function Router({
basename: basenameProp = "/",
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator,
static: staticProp = false,
}: RouterProps): React.ReactElement | null {
// 생략...
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
}
// Route
export function Route(
_props: PathRouteProps | LayoutRouteProps | IndexRouteProps
): React.ReactElement | null {
invariant(
false,
`A <Route> is only ever to be used as the child of <Routes> element, ` +
`never rendered directly. Please wrap your <Route> in a <Routes>.`
);
}
// Routes
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
let dataRouterContext = React.useContext(DataRouterContext);
// 생략...
let routes =
dataRouterContext && !children
? (dataRouterContext.router.routes as DataRouteObject[])
: createRoutesFromChildren(children);
return useRoutes(routes, location);
}
// createRoutesFromChildren
export function createRoutesFromChildren(
children: React.ReactNode,
parentPath: number[] = []
): RouteObject[] {
let routes: RouteObject[] = [];
// 생략 ... (에러처리 및 중첩 routes? 추가)
routes.push(route);
});
return routes;
}
// useRouters
/**
* Returns the element of the route that matched the current location, prepared
현재 주소와 일치하는 element를 반환하는 로직이 여기 들어있다.
*/
export function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
): React.ReactElement | null {
// 생략 ...
let renderedMatches = _renderMatches(
// 생략 ...
);
if (locationArg) {
return (
<LocationContext.Provider
value={{
// 생략 ...
},
navigationType: NavigationType.Pop,
}}
>
{renderedMatches}
</LocationContext.Provider>
);
}
return renderedMatches;
}
대에충 정리해보자면 Context API를 통해 상태를 관리하고 Router > Routers > Router 이렇게 계층 구조를 가진다. 렌더링을 결정하는 로직은 Routers 안에서 useRoutes hook을 통해 결정한다.
// Route
interface RouteProps {
path: string;
component: React.ReactNode;
}
export const Route = ({ component }: RouteProps) => {
return <>{component}</>;
};
// Router
interface RouterProps {
children: React.ReactElement<RouteProps>[];
}
export const Router = ({ children }: RouterProps) => {
const currentPath = usePath();
return (
<>
{children?.map((router: React.ReactElement<RouteProps>) => {
if (router.props.path == currentPath) return router;
})}
</>
);
};
// useRouter
const useRouter = () => {
const push = (path: string) => {
history.pushState(null, "", path);
window.dispatchEvent(new Event("popstate"));
};
return { push };
};
export default useRouter;
// usePath
const usePath = () => {
const [path, setPath] = useState(window.location.pathname);
const updatePath = () => {
setPath(window.location.pathname);
};
useEffect(() => {
window.addEventListener("popstate", updatePath);
return () => {
window.removeEventListener("popstate", updatePath);
};
}, []);
return path;
};
export default usePath;