본문 바로가기
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;
반응형

댓글