FrontEnd/[리액트 2부] 고급 주제와 훅

[2.4장 다이얼로그1][2.5장 다이얼로그2]

Tony Lim 2024. 3. 7. 11:01

Dialog

export const layoutContext = React.createContext({});
layoutContext.displayName = "LayoutContext";

export class Layout extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      dialog: <Dialog></Dialog>,
    };
  }

  render() {
    const value = {
      dialog: this.state.dialog,
    };
    return (
      <layoutContext.Provider value={value}>
        {this.props.children}
      </layoutContext.Provider>
    );
  }
}

export const DialogContainer = () => (
  <layoutContext.Consumer>
    {({ dialog }) => dialog && <Backdrop>{dialog}</Backdrop>}
  </layoutContext.Consumer>
);

DialogContainer는 Provider에서 제공한 Dialog를 조건부 rendering한다.

const Page = ({ header, children, footer }) => (
  <div className="Page">
    <header>{header}</header>
    <main>{children}</main>
    <footer>{footer}</footer>
    <MyLayout.DialogContainer></MyLayout.DialogContainer>
  </div>
);

이를 모든 Page rendering을 담당하는 Page 에 추가하였다. dialog (일종의 팝업)은 어떤 페이지에서든 나타날 수 있기 때문이다.

const App = () => (
  <MyLayout.Layout>
    <MyRouter.Router>
      <MyRouter.Routes>
        <MyRouter.Route path="/cart" element={<CartPage />} />
        <MyRouter.Route path="/order" element={<OrderPage />} />
        <MyRouter.Route path="/" element={<ProductPage />} />
      </MyRouter.Routes>
    </MyRouter.Router>
  </MyLayout.Layout>
);

어떤 곳이든 Provider의 영향을 받아야하기 때문에 최상단 Component로 감싸져있다.

export const withLayout = (WrappedComponent) => {
  const WithLayout = (props) => (
    <layoutContext.Consumer>
      {({ dialog, setDialog }) => {
        const openDialog = setDialog;
        const closeDialog = () => setDialog(null);

        const enhancedProps = {
          dialog,
          openDialog,
          closeDialog,
        };
        return <WrappedComponent {...props} {...enhancedProps} />;
      }}
    </layoutContext.Consumer>
  );
  WithLayout.displayName = `WithLayout(${getComponentName(WrappedComponent)})`;
  return WithLayout;
};

export const DialogContainer = withLayout(
  ({ dialog }) => dialog && <Backdrop>{dialog}</Backdrop>
);

withRouter처럼 고차컴포넌트로 withLayout을 하나 만들엇다.

export const DialogContainer = withLayout(
  ({ dialog }) => dialog && <Backdrop>{dialog}</Backdrop>
);

기존과 동일하게 enhancedProps를 주입받아서 Consumer 같은 boilerplate없이 쉽게 사용가능 해졌다.

 


현재 padding-bottom 값이 따로 정의 되어 있지 않기 때문에 상위 element css로부터 영향을 받게 된다.

    <style>
      .red {
        background-color: red;
      }
      .green div{
        background-color: green;
      }
    </style>
const root = ReactDOM.createRoot(document.getElementById("root"));
const root2 = ReactDOM.createRoot(document.getElementById("dialog"));

// root.render(<App />);

const Red = () => <div className="red">Red</div>;
const Green = () => (
  <div className="green">
    <div>Green</div>

    <Red></Red>
  </div>
);

root.render(<Green></Green>);
root.render(<Red></Red>);

아예 부모를 다르게 Componet들을 render 하면 Red 가 green color로 override 되지 않지만 Green 의 하위 component로 Red Component로 해주고 싶을 수 있다. 이떄 사용할 수 있는게 리액트 포탈이다.

컴포넌트 트리는 마치 Red가 Green 하위 컴포넌트 인것처럼 동작하기 때문에 Red에서 발생한 이벤트가 bubbling되어서 상위 컴포넌트인 Green에게까지 전달이 될 수 있다.

const root = ReactDOM.createRoot(document.getElementById("root"));
const root2 = ReactDOM.createRoot(document.getElementById("dialog"));

// root.render(<App />);

import ReactDOM2 from "react-dom";
const Red = () =>
  ReactDOM2.createPortal(
    <div className="red">Red</div>,
    document.getElementById("dialog")
  );
const Green = () => (
  <div className="green" onClick={(e) => console.log(e)} >
    <div>Green</div>
    <Red></Red>
  </div>
);

root.render(<Green></Green>);

React Portal을 사용하니 실제 Dom tree는 서로 별도의 Dom을 가지는 것으로 나온다.

또한 Red 를 클릭했을때 발생하는 click event가 잘 bubbling 되는것을 확인할 수 있다.

export const DialogContainer = withLayout(
  ({ dialog }) =>
    dialog &&
    ReactDOM.createPortal(
      <Backdrop>{dialog}</Backdrop>,
      document.querySelector("#dialog")
    )
);

portal을 사용하여 Dialog은 다른 DOM에 mount되게 하지만 여전히 Dialog를 쓰는 Page의 child component로 동작한다.


const PaymentSuccessDialog = ({ navigate, closeDialog }) => {
  const handleClickNo = () => {
    closeDialog();
    navigate("/");
  };

  const handleClickYes = () => {
    closeDialog();
    navigate("/order");
  };

  return (
    <Dialog
      header={<>결제 완료</>}
      footer={
        <>
          <Button style={{ marginRight: "8px" }} onClick={handleClickNo}>
            아니오
          </Button>
          <Button styleType="brand" onClick={handleClickYes}>
            네, 주문상태를 확인합니다.
          </Button>
        </>
      }
    >
      결제가 완료되었습니다. 주문 상태를 보러 가시겠습니까?
    </Dialog>
  );
};

export default MyLayout.withLayout(MyRouter.withRouter(PaymentSuccessDialog));

cart page에서 주문을 하고나서 성공을 하면 호출되는 component이다. yes ,no로 어떤곳으로 navigate할지 결정하게 된다.

  async handleSubmit(values) {
    const { startLoading, finishLoading, openDialog } = this.props;

    startLoading("결제중...");
    try {
      await OrderApi.createOrder(values);
    } catch (e) {
      openDialog(<ErrorDialog />);
      return;
    }
    finishLoading();

    openDialog(<PaymentSuccessDialog />);
  }

cart page에서 주문 결제 api를 호출할 때 성공시 호출하는것을 확인할 수 있다.


Dialog 개선: Ref

굳이 사용자가 가르키키전에 Ref를 통해 원하는 element를 focus 할 수 있다.

class Dialog extends React.Component {
  constructor(props) {
    super(props);
    this.footerRef = React.createRef();
  }

  componentDidMount() {
    if (!this.footerRef.current) return;

    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를 Class Component 로 변경하여서 state에 footer에 Ref를 적용한다.

PaymentSucessDialog에서 footer props값으로 yes or no button을 넘겨줬기 때문이다.

componentDidMount 시점에 button element중 가장 마지막에 focus를 주도록한다.