react-router를 구현해보자
date
Oct 7, 2022
thumbnail
slug
react-router를 구현해보자
author
status
Published
tags
React
summary
원티드 프리온보딩 챌린지 첫번째 과제
React와 History API 사용하여 SPA Router 기능 구현하기
를 해결해봅시다.
이미 글 쓰는 시점에서는 전부 개발하고 제출했지만 고민했던 점을 정리해보려고 합니다.type
Post
updatedAt
Jun 17, 2023 01:26 PM
react-router-dom
을 보면 컴포넌트 구조가 아래처럼 Routes
내부에 Route
이 들어있는 구조인데 이것을 그대로 재연하고 싶었다.const Layout = () => { return ( <> <Navigation /> <Routes> <Route path={'/'} element={<App />} /> <Route path={'/1'} element={<Route1 />} /> <Route path={'/2'} element={<Route2 />} /> </Routes> </> ); };
평소에도
HOC
는 많이 개발해봤지만 내부에서 여러개의 children
이 들어간 것은 개발해본 적이 없어서 냅다 콘솔에 찍어봤는데 ReactNode[]
형태로 들어오는 것을 확인했다.type RoutesProps = { children: React.ReactNode | React.ReactNode[]; }; const Routes = ({ children }: RoutesProps) => { const { path } = useContext(RouteContext); const isArray = Array.isArray(children); const isChild = (child: React.ReactNode) => { const { props } = child as React.ReactElement; return props.path === path.pathname; }; const childElement = (child: React.ReactNode) => { const { props } = child as React.ReactElement; return props.element; }; const routeChildren = () => { if (isArray) { return childElement(children.find(isChild)); } else { return childElement(children); } }; const result = routeChildren(); return <>{result}</>; };
그래서 위 코드처럼 children 타입을 정했고 각각 분기처리로 return해주었다.
type RouteProps = { path: string; element: React.ReactNode; }; const Route = ({ path, element }: RouteProps) => { return null; };
Route는 따로 설정할 기능이 없어서 Props type만 지정해줬다.
이제부터가 본격적인 기능 개발인데 React에서 routing을 해본 사람은 알겠지만 Provider를 씌워준다.
저 Provider가 무슨 기능을 할까.. 생각해봤는데 디자인시스템 만들 때와 비슷하게 location 정보들을 담고 상태를 관리해주는 역할을 해주면 된다고 생각했다.
const noop = () => {}; type RouteContextType = { path: PathType; setPath: (location: PathType) => void; }; export const RouteContext = createContext<RouteContextType>({ path: { pathname: location.pathname, search: location.search, hash: location.hash, location: location, }, setPath: noop, });
그래서 간단하게 Context를 정의하고 Provider를 만들어줬다.
type RouteProviderProps = { children: React.ReactNode; }; const RouteProvider = ({ children }: RouteProviderProps) => { const contextValue = useLocation(); return ( <RouteContext.Provider value={contextValue}> {children} </RouteContext.Provider> ); }; export default RouteProvider;
context에 값을 넣어줘야하니 hook도 만들어서 값을 넣어줬다.
export const useLocation = () => { const [path, setPath] = React.useState({ pathname: window.location.pathname, search: window.location.search, hash: window.location.hash, location: window.location, }); return { path, setPath }; };
여기까지만 하면 문제점이 브라우저의 뒤로가기 앞으로가기를 했을 때 화면이 다시 그려지지 않는다는 것이다.
여기서는
popState
를 활용하면 되는데 페이지가 이동되면 실행되는 이벤트이다.useLocation
hook에 추가해줌으로서 브라우저 history
이벤트가 발생했을 때 상태를 변경할 수 있도록 했다.React.useEffect(() => { window.addEventListener('popstate', () => { setPath((prevState) => ({ ...prevState, location: window.location, })); }); return () => { window.removeEventListener('popstate', () => { setPath((prevState) => ({ ...prevState, location: window.location, })); }); }; }, []);
여기까지만 하면 심심하니 navigate함수도 만들어보자.
react-router-dom에서 navigate함수를 만들려면
const navigate = useNavigate()
로 사용해야하는데 이것도 동일하게 커스텀 hook을 생성해봤다.export const useNavigate = () => { let { path, setPath } = useContext(RouteContext); const navigate = useCallback( (inputPath: string) => { if (path.pathname === inputPath) return; history.pushState({}, '', inputPath); setPath({ pathname: location.pathname, search: location.search, hash: location.hash, location: window.location, }); }, [path], ); return navigate; };
여기서는 Provider 내부에서 사용하는 상태이니
Context
를 연결해주고 navigate 함수에 props
로 경로를 받으면 해당 경로로 이동하는 pushState를 생성했다.그리고 화면을 다시 그려줘야하니! setPath로 상태도 변경해주었다.
1. 고민했던 점
여기서 Context에 location하나만 할당하지 않고 여러 value를 넣은 이유가 있는데 location을 직접 context에 집어넣게 되면 context를 통해 상태를 변경했을 때 상태의 변경으로 처리하지 않는다는 점이었다.
생각해보면 location은 Object 즉 참조형 데이터이기 때문에 내부 value값이 바뀐다고 해서 상태의 변경으로 처리하지 않을 것 같다.이전 값, 이후 값을 비교 검증해본 결과 true로 나온다.
이 문제를 해결하려고 생각한 방식이 깊은 복사이다.
const newObj = Object.assign({}, location);
이 코드로 객체를 복사한 후에 다시 재할당하는 방식으로 진행했다.Object.assign이 깊은 복사는 아니지만 location 데이터 구조를 보면 위치 관련 데이터는 전부 일차원 데이터이기 때문에 문제가 없다고 판단했다.
export const useNavigate = () => { let { path, setPath } = useContext(RouteContext); const navigate = useCallback((inputPath: string) => { if (path.pathname === inputPath) return; history.pushState({}, '', inputPath); const newObj = Object.assign({}, location); setPath(newObj); }, []); return navigate;
위 코드로 변경함으로서 해결했다.
너무 재밌었다.