본문 바로가기
Dev/reactJS

여러가지 리액트 멀티 레이아웃 connected-react-router multi layout 적용하기

by 하양동백 2021. 3. 25.

목차

    리액트를 별로 좋아하지 않는 1인이다.

    그런데 어쩔 수 없이 리액트 플젝을 해야 해서 퍼블리셔 겸 프런트 엔드로 참여 중이다.

    jquery라면 단박에 끝낼 문제에 고민하지 않아도 될 문제들이 리액트에는 넘쳐난다.

    왜 쓰는지는 여전히 모르겠지만 까라니까 깐다.

    일단 SPA 사이트에서 리액트가 편한 것은 대충 짐작은 된다.

    그런데 SPA사이트라고는 해도, 한 사이트에 서로 다른 레이아웃이 여럿 사용해야 되는 경우가 많다.

    단적으로 GNB, LNB가 없는 Intro페이지, Login페이지 그리고 사용자 화면과 다른 관리자 화면의 레이아웃은 서로 다를 수 있다. 아니 다를 수 있는 정도가 아니라 대체로 다르다.

    그럴 때 리액트 여러 가지 레이아웃 적용하는 방법은 꽤 다양하지만 직관적이고 쉬운 리액트 멀티 레이아웃 적용 방법을 소개하겠다.

    사실 구글링 해서 나오는 수많은 react multi layout 설명들은 지들끼리만 아는 얘기를 하는 건지, 자기들도 모르는 건지 도무지 알아들을 수 없거나, 또 새로운 라이브러리를 추가하는 방법만 설명하고 있다.

    여기서 소개할 multi layout은 기본적으로 react에서 사용하는 route 기본 라이브러리인 react-router 만으로 해결할 수 있는 방법이다.

    제목에 connected-react-router라고 써 놓은 것에 대해서는 쫄 필요가 없다.

    진행 중인 프로젝트에서 connected-react-router를 사용하고 있어서 예시를 그렇게 들었을 뿐이다.

    connected-react-router는 redux 때문에 쓰는 것이니, 어쩌면 다른 프로젝트들에서도 기본적으로 쓰는 것일 수 있다.

    전체 소스를 공개하기에는 현재 프로젝트의 보안서약 때문에 불가하고, 멀티 레이어의 핵심만 언급하겠다.

    리액트 멀티 레이아웃 react multi layout

    import { Route, Switch } from "react-router"; // react-router v4/v5
    import { ConnectedRouter } from "connected-react-router";
    
    // Layout
    import CampaignLayout from "./layouts/CampaignLayout";
    import DemoGuideLayout from "./layouts/DemoGuideLayout";
    
    
    // Sub Children
    import CampaignPolicyLayout from "./domain/CampaignPolicy/CampaignPolicyLayout";
    import CampaignPolicyFormContainer from "./domain/CampaignPolicy/CampaignPolicyFormContainer";
    import CampaignPolicyInfoContainer from "./domain/CampaignPolicy/CampaignPolicyInfoContainer";
    import DemoFormList from "./domain/DemoGuide/DemoFormListLayout";
    import DemoGuideFormContainer from "./domain/DemoGuide/CampaignPolicyFormContainer";
    import DemoGuideInfoContainer from "./domain/DemoGuide/CampaignPolicyInfoContainer";
    
    // Login without Layout
    import { default as MemberLogin } from "./domain/Member";
    
    
    function App({ history, context }) {
      return (
        <ConnectedRouter history={history} context={context}>
          <Switch>
            <Route exact path="/MemberLogin" component={MemberLogin} />
            {/* 서로 다른 레이아웃은 피어한 Route */}
            <Route path="/campaign-policy/:path?" exact>
              {/* 레이아웃 호출 */}
              <CampaignLayout>
              {/* 같은 레이아웃을 공유하는 서브메뉴들은 하나의 Switch에서 Route됨 */}
                <Switch>
                  <Route
                    exact
                    path="/campaign-policy"
                    component={CampaignPolicyLayout}
                  />
                  <Route
                    exact
                    path="/campaign-policy/create"
                    component={CampaignPolicyFormContainer}
                  />
                  <Route
                    exact
                    path="/campaign-policy/:id"
                    component={CampaignPolicyInfoContainer}
                  />
                </Switch>
              </CampaignLayout>
            </Route>
            <Route path="/demo-guide/:path?" exact>
              {/* 레이아웃 호출 */}
              <DemoGuideLayout>
              {/* 같은 레이아웃을 공유하는 서브메뉴들은 하나의 Switch에서 Route됨 */}
                <Switch>
                  <Route exact path="/demo-guide/" component={DemoFormList} />
                  <Route
                    exact
                    path="/demo-guide/create"
                    component={DemoGuideFormContainer}
                  />
                  <Route
                    exact
                    path="/demo-guide/:id"
                    component={DemoGuideInfoContainer}
                  />
                </Switch>
              </DemoGuideLayout>
            </Route>
            <ToastNotiComponent />
          </Switch>
        </ConnectedRouter>
      );
    }
    
    export default App;

    꽤 길어 보이지만, 사실 3가지의 Route가 사용되었다.

    첫 번째 Switch에서 3가지 레이아웃이 분기된다.

    /MemberLogin에서 분기된 레이아웃에는 특별한 레이아웃이 없다.

    해당 페이지 자체가 레이아웃 형상을 갖고 있다.

    /campaign-policy/ 경로에서 사용되는 레이아웃은 <CampaignLayout></CampaignLayout> 안에서 Switch 되어 메뉴별로 분기된다.

    만약 /campaign-policy/ 와 레이아웃을 공유하는 /campaign-policy2/서브메뉴가 있다면 다음과 같이 적용할 수 있다.

    import { Route, Switch } from "react-router"; // react-router v4/v5
    import { ConnectedRouter } from "connected-react-router";
    
    // Layout
    import CampaignLayout from "./layouts/CampaignLayout";
    import DemoGuideLayout from "./layouts/DemoGuideLayout";
    
    
    // Sub Children
    import CampaignPolicyLayout from "./domain/CampaignPolicy/CampaignPolicyLayout";
    import CampaignPolicyFormContainer from "./domain/CampaignPolicy/CampaignPolicyFormContainer";
    import CampaignPolicyInfoContainer from "./domain/CampaignPolicy/CampaignPolicyInfoContainer";
    import DemoFormList from "./domain/DemoGuide/DemoFormListLayout";
    import DemoGuideFormContainer from "./domain/DemoGuide/CampaignPolicyFormContainer";
    import DemoGuideInfoContainer from "./domain/DemoGuide/CampaignPolicyInfoContainer";
    
    // Login without Layout
    import { default as MemberLogin } from "./domain/Member";
    
    
    function App({ history, context }) {
      return (
        <ConnectedRouter history={history} context={context}>
          <Switch>
            <Route exact path="/MemberLogin" component={MemberLogin} />
            {/* 서로 다른 레이아웃은 피어한 Route */}
            <Route path="/campaign-policy/:path?" exact>
              {/* 레이아웃 호출 */}
              <CampaignLayout>
              {/* 같은 레이아웃을 공유하는 서브메뉴들은 하나의 Switch에서 Route됨 */}
                <Switch>
                  <Route
                    exact
                    path="/campaign-policy"
                    component={CampaignPolicyLayout}
                  />
                  <Route
                    exact
                    path="/campaign-policy/create"
                    component={CampaignPolicyFormContainer}
                  />
                  <Route
                    exact
                    path="/campaign-policy/:id"
                    component={CampaignPolicyInfoContainer}
                  />
                </Switch>
              </CampaignLayout>
            </Route>
            <Route path="/campaign-policy2/:path?" exact>
              {/* 레이아웃 호출 */}
              <CampaignLayout>
              {/* 같은 레이아웃을 공유하는 서브메뉴들은 하나의 Switch에서 Route됨 */}
                <Switch>
                  <Route
                    exact
                    path="/campaign-policy2"
                    component={CampaignPolicyLayout}
                  />
                  <Route
                    exact
                    path="/campaign-policy2/create"
                    component={CampaignPolicyFormContainer}
                  />
                  <Route
                    exact
                    path="/campaign-policy2/:id"
                    component={CampaignPolicyInfoContainer}
                  />
                </Switch>
              </CampaignLayout>
            </Route>
            <Route path="/demo-guide/:path?" exact>
              {/* 레이아웃 호출 */}
              <DemoGuideLayout>
              {/* 같은 레이아웃을 공유하는 서브메뉴들은 하나의 Switch에서 Route됨 */}
                <Switch>
                  <Route exact path="/demo-guide/" component={DemoFormList} />
                  <Route
                    exact
                    path="/demo-guide/create"
                    component={DemoGuideFormContainer}
                  />
                  <Route
                    exact
                    path="/demo-guide/:id"
                    component={DemoGuideInfoContainer}
                  />
                </Switch>
              </DemoGuideLayout>
            </Route>
            <ToastNotiComponent />
          </Switch>
        </ConnectedRouter>
      );
    }
    
    export default App;

    그리고 demo-guide 경로에서는 <DemoGuideLayout></DemoGuideLayout> 레이아웃을 사용한다.

    레이아웃 측에서는 다음과 같이 내용 콘텐츠를 위치시킨다.

    import React, { useState, useEffect, Component } from "react";
    import { useSelector } from "react-redux";
    
    import classNames from "classnames/bind";
    // left menu
    import SideMenu from "../components/SideMenu/SideMenu";
    // header
    import Header from "../components/Header/Header";
    import styles from "../App.css";
    
    const cx = classNames.bind(styles);
    // 컴포넌트 정의
    const CampaignLayout = ({children}) => {
      const leftSize = useSelector((state) => state.menuStore.leftSize);
      const oLeftSize = useSelector((state) => state.menuStore.oLeftSize);
      let sideMenu = null;
      // let sideMenu = null;
      function getWindowDimensions() {
        const { innerWidth: width, innerHeight: height } = window;
        return {
          width,
          height,
        };
      }
      const [windowDimensions, setWindowDimensions] = useState(
        getWindowDimensions()
      );
    
      useEffect(() => {
        function handleResize() {
          setWindowDimensions(getWindowDimensions());
        }
    
        window.addEventListener("resize", handleResize);
        return () => window.removeEventListener("resize", handleResize);
      }, []);
    
      // render
      return (
        <>
          <div className={cx("root")}>
            <SideMenu />
            <div
              className={cx("container0")}
              rel={windowDimensions.height}
              style={{
                transform: `translate3d(${
                  leftSize === oLeftSize ? "0" : `-${oLeftSize}px`
                }, 0, 0)`,
                width: `calc(100% - ${leftSize}px)`,
                left: `${oLeftSize}px`,
                height: `100vh`,
              }}
            >
              <Header />
              <div className={cx("contents")}>{children}</div>
            </div>
          </div>
        </>
      );
    };
    
    export default CampaignLayout;
    

    복잡해 보이겠지만 중요한 핵심은 다음과 같다.

    const CampaignLayout = ({children}) => {
      // render
      return (
        <>
            {children}
        </>
      );
    };
    
    export default CampaignLayout;
    
    반응형

    댓글