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;
위 코드로 변경함으로서 해결했다.
너무 재밌었다.