ET네 만물상 프로젝트 복기 - 커스텀 Router

DD·2021년 9월 6일
0

우아한 테크캠프

목록 보기
11/14
post-thumbnail

📣 이 시리즈는...

  • 우아한 테크캠프 4기에서 마지막으로 진행한 프로젝트 전체를 복기해본 문서 시리즈입니다.
  • 제가 작성하지 않은 코드도 포함해서 복기했기에, 오류가 있을 수도 있는 개인적인 학습 기록을 위한 문서입니다!

ET네 만물상 - GitHub Repository / 배포 링크

  • 현재 배포 링크는 내부 문제로 API서버가 동작하지 않습니다.. 조만간 해결할 예정..



커스텀 Router

  • 리액트에서 SPA를 개발하기 위한 라우터

App

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],
];
  • 먼저 경로, 컴포넌트 함수, exact 정보를 담고있는 배열 형태의 route가 필요하고, 이 배열을 담고 있는 routes 배열이 필요하다.
  • 참고로 exact는 해당 경로와 정확히 일치해야만 하는지에 대한 정보다. 기본값은 false
const App = () => {
  return (
    <Router>
      {routes.map(([path, component, exact]: RouteSetType) => (
        <Route
          path={path}
          exact={exact ?? false}
          key={path}
          component={component}
        />
      ))}
    </Router>
  );
};
  • 라우터를 사용하는 구조는 이렇게 생겼다. 이제 각 요소들을 하나씩 살펴보자



유틸함수

1. checkPath

const checkPath = (
  targetPath: string,
  currPath: string,
  exact: boolean
): boolean => {
  if (exact) {
    return targetPath === currPath;
  } else {
    return currPath.match(new RegExp(targetPath, "i"))?.index === 0;
  }
};
  • 현재 path / 타겟 path 둘을 비교하는 함수인데, exact 옵션이 있다면 정확하게 일치하는지, 없다면 일부(맨 앞)가 일치하는지 boolean 값을 반환한다
  • 이 함수는 Router, Route에서 쓰인다.

2. moveTo

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에 등록한 리스너가 감지해서 동작할 것이다!


3. decodeParams

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 객체에 담는다.




Router

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,
  },
});
  • 먼저 위에서 본 것 처럼 Routerroutes배열의 각 요소를 props로 전달 받은 Route 컴포넌트들을 children으로 받는다.

  • recoil을 사용해서 전역으로 관리되고 있는 현재 로그인 상태 / 현재 location / 선택된 카테고리. 3가지 상태를 받아온다.

  • Router는 총 6개의 지역함수를 가지고 있다.


1. checkPathValidation

const checkPathValidation = () => {
  const exist = routes.find(([path, component, exact]: RouteSetType) =>
    checkPath(path, window.location.pathname, exact)
  );

  !exist && moveTo(NOT_FOUND);
};
  • checkPath를 이용해서 routes 배열에서 현재 path와 일치하는 route가 있는지 find한다.
  • 존재하지 않는다면 NOT_FOUND 페이지로 이동시킨다.

2. addEvents

const addEvents = () => {
  window.addEventListener("pushstate", handlePushState);
  window.addEventListener("popstate", handlePopState);
};
  • 단순하다. pushstate, 'popstate이벤트 리스너를 등록한다. RouteruseEffect`의 콜백에서 실행 될 것

3. setCurrentCategory

const setCurrentCategory = () => {
  const params = decodeParams();

  setSelectedCategoryState({
    categoryId: params.category ? parseInt(params.category) : -1,
    subCategoryId: params.subCategory && parseInt(params.subCategory),
  });
};
  • 이 함수는 사실 커스텀 라우터의 기능이라기보다, 프로젝트에서 사용하는 카테고리와 관련한 커스텀 커스텀 기능이라고 볼 수 있다. 한 마디로 우리 프로젝트에서 사용하기 때문에 필요한 것..

  • 현재 url의 query를 추출해서 카테고리 id와 서브카테고리 id 상대를 업데이트한다


4. setCurrentLocation

const setCurrentLocation = () => {
  setLocation({
    location: window.location.pathname,
    params: decodeParams(),
  });

  document.documentElement.scrollTo(0, 0);
  setCurrentCategory();
};
  • 현재 pathname과 query를 가져와 상태를 set한다.
  • 스크롤을 상단으로 올리고 현재 상태의 카테고리를 set한다

5. handlePushState / handlePopState

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를 호출한다




Route

export const Route = ({ exact, path, component: Component }: RouterType) => {
  const { location } = useRecoilValue(locationState);

  return checkPath(path, location, exact) ? <Component /> : null;
};
  • 현재 url과 비교해서 일치하면 해당 컴포넌트를 return하고, 아니면 null을 반환한다.
  • Router에서 routes 배열을 돌렸을 때 해당하는 Route만 남고 나머지가 null이 되는 것
  • 이 원리로 현재 url과 exact한 것만 남거나, exact false인 경우 부분 일치한 경우만 남게된다.
  • 우리 프로젝트의 경우 일부 일치하는 경우의 routes를 구성하진 않았다. 따라서 하나의 컴포넌트만 살아남게(?) 된다.



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;
`;
  • a링크대신 클릭하면 moveTo로 지정한 url을 전달해서 커스텀 pushstate 이벤트를 발생시키도록 한다.
  • 이후에는 이미 등로한 리스너가 pushstate를 감지해서 동작할 것이다.



정리

    1. 최상단 컴포넌트(APP)에서 routes 배열을 준비한다
    1. Router 컴포넌트의 자식으로 routes.map을 통해 각 요소를 Route에 전달한다.
    1. Route는 현재 url과 일치하는 경우에만 해당 component를 return한다
    1. pushstate 이벤트가 발생하면 현재 pathname을 history에 저장하고 전달받은 pathname을 loaction에 추가한다 (이동)
    • 이 과정을 통해 뒤로가기가 구현된다

기본 흐름은 이러하고, 디테일한 내용은 각 함수 설명글을 참고하자..

profile
기억보단 기록을 / TIL 전용 => https://velog.io/@jjuny546

2개의 댓글

comment-user-thumbnail
2021년 9월 10일

라이브러리를 사용하지 않고 직접 구현한 것은 라이브러리에서 제공하지 않는 기능이나 결함이 있어서 인가요?! 아니면 학습을 위해서?? 궁금하네요 ^0^

1개의 답글