지난 포스팅에서 다룬 Router Context는 주소와 관련된 작업을 수행하기 위해 구현되었다. 이번 포스팅에서 다룰 Layout Context는 각 화면에서 사용할 Dialog를 위해 구현하였다.
전반적인 아키텍처는 Router Context와 비슷하다!
📁 Dialog.jsx
import React from "react";
class Dialog extends React.Component {
constructor(props) {
super(props);
this.footerRef = React.createRef();
}
componentDidMount() {
if (!this.footerRef.current) return;
// footer에서 button을 모두 찾는데, 유사 배열이므로 Array로 만듬.
const buttons = Array.from(
this.footerRef.current.querySelectorAll("button")
);
if (!buttons.length === 0) return;
const activeButton = buttons[buttons.length-1]; // 가장 오른쪽 버튼
activeButton.focus();
}
render() {
const { header, children, footer } = this.props;
return (
<div className="Dialog">
{header && <header>{header}</header>}
<main>{children}</main>
{footer && <footer ref={this.footerRef}>{footer}</footer>}
</div>
);
}
}
export default Dialog;
먼저 기본적인 Dialog를 구현한다. 본래는 함수 컴포넌트로 구현했으나, 후에 createRef()를 통한 ref 객체를 사용하기 위해 클래스 컴포넌트로 변경하였다.
기본적인 기능은 header, footer 그리고 children을 props로 받아 렌더링하는데, 이 때 header와 footer는 조건부로 렌더링한다.
footerRef
는 footer에 버튼이 들어올 경우 기본 값에 대한 포커싱을 적용하기 위해 사용하였는데, 컴포넌트가 마운트되었을 때 모든 button 태그들을 찾아 가장 오른쪽 버튼에 포커싱되도록 한다.
이를 적용하면 Dialog가 렌더링되었을 때 기본값 버튼을 엔터키로 선택하거나, Shift+Tap을 통해 다른 버튼을 선택할 수 있다.
먼저 footer에 버튼이 두 개 존재할 때이다. 오른쪽 버튼인
예, 주문 상태를 확인합니다.
버튼에 포커싱이 된 것을 확인할 수 있다. 따라서 ✨고객이 해당 버튼을 누를 수 있도록 유도하는 효과✨를 준다.
footer에 버튼이 한 개 존재할 때이다. 이 때는 유일하게 존재하는 버튼에 포커싱이 된 것을 확인할 수 있다.
📁 MyLayout.jsx
import React from "react";
import ReactDOM from "react-dom";
import Dialog from "../components/Dialog";
import Backdrop from "../components/BackDrop";
import { getComponentName } from "./utils";
export const layoutContext = React.createContext({});
layoutContext.displayName = "LayoutContext";
export class Layout extends React.Component {
constructor(props) {
super(props);
this.state = {
dialog: <Dialog />,
};
// 비동기적으로 동작하기 때문에 this 바인딩
this.setDialog = this.setDialog.bind(this);
}
setDialog(dialog) {
this.setState({ dialog });
}
render() {
const value = {
dialog: this.state.dialog,
setDialog: this.setDialog,
};
return (
<layoutContext.Provider value={value}>
{this.props.children}
</layoutContext.Provider>
);
}
}
export const withLayout = (WrappedComponent) => {
const WithLayout = (props) => (
<layoutContext.Consumer>
{({ dialog, setDialog }) => {
const openDialog = setDialog;
const closeDialog = () => setDialog(null);
// 좀 더 개선된 버전
const startLoading = (message) =>
openDialog(<Dialog>{message}</Dialog>);
const finishLoading = closeDialog;
const enhancedProps = {
dialog,
openDialog,
closeDialog,
startLoading,
finishLoading,
};
return <WrappedComponent {...props} {...enhancedProps} />;
}}
</layoutContext.Consumer>
);
WithLayout.displayName = `WithLayout(${getComponentName(WrappedComponent)})`;
return WithLayout;
};
// dialog 상태에 따라 노출시키기
export const DialogContainer = withLayout(
({ dialog }) =>
dialog &&
ReactDOM.createPortal(
<Backdrop>{dialog}</Backdrop>,
document.querySelector("#dialog")
)
);
위에서 구현한 Dialog를 사용하는 Layout Context 파일이다. createContext() 함수를 이용해 layoutContext 객체를 생성해준다. 크게 보면 Provider를 사용하는 Layout
컴포넌트와 Consumer를 사용하는 withLayout
컴포넌트로 나눌 수 있다.
🅿 먼저 Layout
컴포넌트는 하위 트리에 전달할 값을 가지고 있어야 하고, 그 값은 렌더링할 dialog와 그 dialog 값을 변경시킬 setDialog 함수여야 한다. 따라서 두 값을 가진 객체를 Provider의 value 속성을 통해 전달한다.
🅲 withLayout
컴포넌트는 지난 시간에 다룬 withRouter
컴포넌트와 마찬가지로, WrappedComponent가 사용하는 props를 받아 재구성한 뒤 enhancedProps 형태로 WrappedComponent에 전달한다.
withLayout
을 통해 실제로 Layout Context를 사용해보자.
장바구니 화면을 렌더링하는 CartPage
컴포넌트를 예시로 들겠다.
export default MyLayout.withLayout(MyRouter.withRouter(CartPage));
지난 시간에 사용한 MyRouter.withRouter에 이어, MyLayout.withLayout으로 컴포넌트를 감싸주었다.
이제 CartPage
컴포넌트는 withLayout
이 가지고 있는 함수들을 사용할 수 있음!
// 콜백 함수: OrderForm을 통해 전달 받은 주문 정보를 가져옴.
async handleSubmit(values) {
const { startLoading, finishLoading, openDialog } = this.props;
startLoading('결제 중 ...');
// api 호출
try {
await OrderApi.createOrder(values);
} catch (e) {
openDialog(<ErrorDialog />);
return;
}
finishLoading();
openDialog(<PaymentSuccessDialog />);
}
매개변수 values를 통해 전달 받은 주문 정보를 사용하는 handleSubmit 함수를 예로 들겠다. handleSubmit 함수는 '결제하기' 버튼을 클릭하면 실행되며, withLayout
컴포넌트가 가지고 있는 함수들은 props를 통해 가져올 수 있다.
PaymentSuccessDialog
컴포넌트가 openDialog 함수를 통해 렌더링된다. 이 때 사용자는 결제 창으로 넘어갈지 선택할 수 있다. ErrorDialog
컴포넌트를 openDialog 함수를 통해 렌더링한다.Layout Context는 이와 같은 방식으로 사용할 수 있다.
그리고 파일 최하단에 구현되어 있는 DialogContainer
컴포넌트는 다음 시간에 React의 createPortal 함수에 대해 다루면서 분석하려고 한다‼️