๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Lect & Tip/node, Angular, React

์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ฆฌ์•กํŠธ ๋ฉ€ํ‹ฐ ๋ ˆ์ด์•„์›ƒ connected-react-router multi layout ์ ์šฉํ•˜๊ธฐ

by ๋‚ฏ์„ ๊ณต๊ฐ„2019 2021. 2. 10.

๋ชฉ์ฐจ

    ๋ฆฌ์•กํŠธ๋ฅผ ๋ณ„๋กœ ์ข‹์•„ํ•˜์ง€ ์•Š๋Š” 1์ธ์ด๋‹ค.

    ๊ทธ๋Ÿฐ๋ฐ ์–ด์ฉ” ์ˆ˜ ์—†์ด ๋ฆฌ์•กํŠธ ํ”Œ์ ์„ ํ•ด์•ผ ํ•ด์„œ ํผ๋ธ”๋ฆฌ์…” ๊ฒธ ํ”„๋ŸฐํŠธ ์—”๋“œ๋กœ ์ฐธ์—ฌ ์ค‘์ด๋‹ค.

    jquery๋ผ๋ฉด ๋‹จ๋ฐ•์— ๋๋‚ผ ๋ฌธ์ œ์— ๊ณ ๋ฏผํ•˜์ง€ ์•Š์•„๋„ ๋  ๋ฌธ์ œ๋“ค์ด ๋ฆฌ์•กํŠธ์—๋Š” ๋„˜์ณ๋‚œ๋‹ค.

    ์™œ ์“ฐ๋Š”์ง€๋Š” ์—ฌ์ „ํžˆ ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ ๊นŒ๋ผ๋‹ˆ๊นŒ ๊น๋‹ค.

    ์ผ๋‹จ SPA ์‚ฌ์ดํŠธ์—์„œ ๋ฆฌ์•กํŠธ๊ฐ€ ํŽธํ•œ ๊ฒƒ์€ ๋Œ€์ถฉ ์ง์ž‘์€ ๋œ๋‹ค.

    ๊ทธ๋Ÿฐ๋ฐ SPA์‚ฌ์ดํŠธ๋ผ๊ณ ๋Š” ํ•ด๋„, ํ•œ ์‚ฌ์ดํŠธ์— ์„œ๋กœ ๋‹ค๋ฅธ ๋ ˆ์ด์•„์›ƒ์ด ์—ฌ๋Ÿฟ ์‚ฌ์šฉํ•ด์•ผ ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.

    ๋‹จ์ ์œผ๋กœ GNB, LNB๊ฐ€ ์—†๋Š” IntroํŽ˜์ด์ง€, LoginํŽ˜์ด์ง€ ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ์ž ํ™”๋ฉด๊ณผ ๋‹ค๋ฅธ ๊ด€๋ฆฌ์ž ํ™”๋ฉด์˜ ๋ ˆ์ด์•„์›ƒ์€ ์„œ๋กœ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค. ์•„๋‹ˆ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋Š” ์ •๋„๊ฐ€ ์•„๋‹ˆ๋ผ ๋Œ€์ฒด๋กœ ๋‹ค๋ฅด๋‹ค.

    ๊ทธ๋Ÿด ๋•Œ ๋ฆฌ์•กํŠธ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ ˆ์ด์•„์›ƒ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๊ฝค ๋‹ค์–‘ํ•˜์ง€๋งŒ ์ง๊ด€์ ์ด๊ณ  ์‰ฌ์šด ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ•˜๊ฒ ๋‹ค.

    ๋ฆฌ์•กํŠธ ๋ฉ€ํ‹ฐ ๋ ˆ์ด์•„์›ƒ react multi layout

    ์‚ฌ์‹ค ๊ตฌ๊ธ€๋ง ํ•ด์„œ ๋‚˜์˜ค๋Š” ์ˆ˜๋งŽ์€ react multi layout ์„ค๋ช…๋“ค์€ ์ง€๋“ค๋ผ๋ฆฌ๋งŒ ์•„๋Š” ์–˜๊ธฐ๋ฅผ ํ•˜๋Š” ๊ฑด์ง€, ์ž๊ธฐ๋“ค๋„ ๋ชจ๋ฅด๋Š” ๊ฑด์ง€ ๋„๋ฌด์ง€ ์•Œ์•„๋“ค์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜, ๋˜ ์ƒˆ๋กœ์šด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•๋งŒ ์„ค๋ช…ํ•˜๊ณ  ์žˆ๋‹ค.

    ์—ฌ๊ธฐ์„œ ์†Œ๊ฐœํ•  multi layout์€ ๊ธฐ๋ณธ์ ์œผ๋กœ react์—์„œ ์‚ฌ์šฉํ•˜๋Š” route ๊ธฐ๋ณธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ react-router ๋งŒ์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

    ์ œ๋ชฉ์— connected-react-router๋ผ๊ณ  ์จ ๋†“์€ ๊ฒƒ์— ๋Œ€ํ•ด์„œ๋Š” ์ซ„ ํ•„์š”๊ฐ€ ์—†๋‹ค.

    ์ง„ํ–‰ ์ค‘์ธ ํ”„๋กœ์ ํŠธ์—์„œ connected-react-router๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด์„œ ์˜ˆ์‹œ๋ฅผ ๊ทธ๋ ‡๊ฒŒ ๋“ค์—ˆ์„ ๋ฟ์ด๋‹ค.

    connected-react-router๋Š” redux ๋•Œ๋ฌธ์— ์“ฐ๋Š” ๊ฒƒ์ด๋‹ˆ, ์–ด์ฉŒ๋ฉด ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ๋“ค์—์„œ๋„ ๊ธฐ๋ณธ์ ์œผ๋กœ ์“ฐ๋Š” ๊ฒƒ์ผ ์ˆ˜ ์žˆ๋‹ค.

    ์ „์ฒด ์†Œ์Šค๋ฅผ ๊ณต๊ฐœํ•˜๊ธฐ์—๋Š” ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์˜ ๋ณด์•ˆ์„œ์•ฝ ๋•Œ๋ฌธ์— ๋ถˆ๊ฐ€ํ•˜๊ณ , ๋ฉ€ํ‹ฐ ๋ ˆ์ด์–ด์˜ ํ•ต์‹ฌ๋งŒ ์–ธ๊ธ‰ํ•˜๊ฒ ๋‹ค.

    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;
    
    ๋ฐ˜์‘ํ˜•

    ๋Œ“๊ธ€