[TIL] 241121_React: Context 적용하기 <Router편>

지코·2024년 11월 21일
0

Today I Learned

목록 보기
56/66
post-thumbnail

⚡️ Context 개념을 Router에 적용하기

지난 시간에 다룬 Context 개념을 우리 프로젝트에 사용할 Router를 구현하며 적용해보려고 한다.

const App = () => {
  const { pathname } = window.location;
  return (
    <>
      {pathname === "/cart" && <CartPage />}
      {pathname === "/order" && <OrderPage />}
      {!["/order", "/cart"].includes(pathname) && <ProductPage />}
    </>
  );
};

기존 App은 위와 같이 pathname에 따라 화면을 조건부 렌더링하는 방식이었다. 이는 페이지 렌더링 시 서버를 사용하는 방식이므로, 브라우저 자체 렌더링 방식으로 변경하기 위해 Router Context를 구현하고자 한다.

📁 MyRouter.jsx

import React from "react";
import { getComponentName } from "./utils";

export const routerContext = React.createContext({});
routerContext.displayName = "RouterContext";

export class Router extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      path: window.location.pathname,
    };
    // 비동기로 동작하므로 this 바인딩
    this.handleChangePath = this.handleChangePath.bind(this);
    this.handleOnPopState = this.handleOnPopState.bind(this);
  }

  handleChangePath(path) {
    this.setState({ path }); // path 값을 인자 값으로 변경
    window.history.pushState({ path }, "", path); // 주소창 주소 변경
  }
  // popState 이벤트가 발행됐을 때(브라우저의 앞/뒤로 가기) 경로 변경하는 함수
  handleOnPopState(event) {
    const nextPath = event.state && event.state.path;

    if (!nextPath) return;
    this.setState({ path: nextPath });
  }

  componentDidMount() {
    // 브라우저의 앞/뒤로 가기 이벤트(popstate)가 발행되면 handleOnPopState 함수를 실행
    window.addEventListener("popstate", this.handleOnPopState);
    // 컴포넌트 렌더링 시 현재 경로로 state 저장 (설정하지 않으면 null)
    window.history.replaceState({ path: this.state.path }, "");
  }

  componentWillUnmount() {
    window.removeEventListener("popstate", this.handleOnPopState);
  }

  render() {
    const contextValue = {
      path: this.state.path,
      changePath: this.handleChangePath,
    };

    return (
      <routerContext.Provider value={contextValue}>
        {this.props.children}
      </routerContext.Provider>
    );
  }
}
// Routes가 정해진 path에만 얽매이지 않고, children으로 받아 렌더링하도록 리팩토링(!) => 재사용성 향상
export const Routes = ({ children }) => {
  return (
    <routerContext.Consumer>
      {({ path }) => {
        let selectedRoute = null;

        React.Children.forEach(children, (child) => {
          if (!React.isValidElement(child)) return; // 리액트 Element인지 검사
          if (child.type === React.Fragment) return; // <></>인지 검사
          if (!child.props.path || !child.props.element) return; // Route 컴포넌트가 맞는지 검사
          if (child.props.path !== path.replace(/\?.*$/, "")) return; // 요청 경로를 검사(query 문자열 제거)

          selectedRoute = child.props.element;
        });

        return selectedRoute;
      }}
    </routerContext.Consumer>
  );
};

export const Route = () => null;

export const withRouter = (WrappedComponent) => {
  const WithRouter = (props) => (
    <routerContext.Consumer>
      {({ path, changePath }) => {
        // 주소 이동하기
        const navigate = (nextPath) => {
          if (path !== nextPath) changePath(nextPath);
        };
        // 주소 일치 여부 확인
        const match = (comparedPath) => comparedPath === path;
        // 쿼리 스트링 이용하기
        const params = () => {
          const params = new URLSearchParams(window.location.search);
          
          const paramsObject = {};
          for (const [key, value] of params) {
            paramsObject[key] = value;
          }
          return paramsObject;
        }

        const enhancedProps = {
          navigate,
          match,
          params
        };

        return <WrappedComponent {...props} {...enhancedProps} />;
      }}
    </routerContext.Consumer>
  );
  WithRouter.displayName = `WithRouter(${getComponentName(WrappedComponent)})`;
  return WithRouter;
};

먼저 createContext 함수를 통해 routerContext 객체를 생성한다. 이 파일은 크게 Provider를 담당하는 Router 컴포넌트와 Consumer를 담당하는 Routes 컴포넌트로 나눌 수 있다.

🅿 Router 컴포넌트를 살펴보자. window 객체를 사용해 현재 pathname을 가져오고, 클래스 내에서 state로 관리한다. 이 때 handleChangePath 함수를 통해 상태 값을 업데이트(setState)하고, 주소를 변경(window.history.pushState)한다.

브라우저의 앞/뒤로 가기 버튼 클릭 시에는 popstate 이벤트가 발행되는데, 이 때 event 객체를 활용해 경로를 변경하는 handleOnPopState 함수를 구현하고 이를 EventListener로 등록한다.
또한 컴포넌트 렌더링 시 현재 경로를 state의 path 값으로 대체하도록 window.history.replaceState 함수를 사용한다.

🧐 여기서 replaceState 함수는 왜 사용할까?

SPA 라우터에서는 브라우저 주소와 컴포넌트의 state에 저장된 path가 동기화되어야 한다. replaceState 함수는 브라우저 히스토리에 초기 상태를 설정해, 브라우저의 앞/뒤로 가기 버튼 이용 시 일관성 유지에 도움을 준다.
⚠️ 만약 replaceState 를 사용하지 않을 경우, 새로고침하거나 브라우저 탐색 버튼을 사용할 때 상태가 null이 될 수도 있다. 따라서 컴포넌트가 마운트되었을 때 해당 작업이 이루어질 수 있도록 해야 한다.

최종적으로 Router 객체는 경로 path와 경로를 변경하는 함수를 Provider의 value 속성을 통해 하위 트리에 전달한다. 따라서 Consumer 컴포넌트들은 모두 이 값을 사용할 수 있다.

🅲 이제 Consumer 컴포넌트인 Routes 컴포넌트를 살펴보자.


Reference

👩🏻‍🏫 [리액트 2부] 고급 주제와 훅
https://www.inflearn.com/course/리액트-고급주제와-훅-2부

📄 React 공식 문서: Context-React
https://ko.legacy.reactjs.org/docs/context.html#before-you-use-context

profile
꾸준하고 성실하게, FE 개발자 역량을 키워보자 !

0개의 댓글