목차
리액트를 별로 좋아하지 않는 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;
댓글