Skip to content

레거시 프로젝트 구성 - CRA 기반 컴포넌트 구현

Create React App 기반 레거시 프로젝트를 구축하고 webpack 설정 없이 Button, Table, Modal, SNS, Routing, Header 컴포넌트를 구현한다

학습 목표

  1. webpack 설정 없는 CRA 레거시 프로젝트의 특징과 제약을 이해한다
  2. 클래스 컴포넌트 기반의 공통 컴포넌트(Button, Table, Modal)를 구현할 수 있다
  3. SNS 컴포넌트와 라우팅/헤더를 작성할 수 있다
  4. custom event와 window 객체를 통한 마이크로앱 Export 패턴을 익힌다

본문

1. 레거시 프로젝트의 위치와 특징

legacy 프로젝트는 docs, web과 동일한 모노레포 내에 존재하지만, webpack 커스텀 설정(config-overrides.js)을 사용하지 않는 순수 CRA 프로젝트다.

2. 프로젝트 초기 설정

bash
npx create-react-app legacy
cd legacy
설정 항목
포트7003 (.env: PORT=7003)
React 버전18.x
컴포넌트 방식클래스 컴포넌트
스타일링순수 CSS
상태관리컴포넌트 로컬 state
webpack 설정없음 (CRA 기본값)

3. Button 공통 컴포넌트

jsx
// components/Button.js
import React from "react";
import "./Button.css";

export default class Button extends React.Component {
  render() {
    const { buttonType = "confirm", children, className = "", ...rest } = this.props;
    return (
      <button className={`btn btn-${buttonType} ${className}`} {...rest}>
        {children}
      </button>
    );
  }
}
css
/* Button.css */
.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  color: white;
}
.btn-confirm { background-color: darkblue; }
.btn-confirm:hover { background-color: blue; }
.btn-cancel { background-color: darkred; }
.btn-cancel:hover { background-color: red; }

4. Table 공통 컴포넌트

web 프로젝트의 Table과 동일한 API를 유지하되 클래스 컴포넌트로 작성한다.

jsx
// components/Table.js
export default class Table extends React.Component {
  render() {
    const { columns, datas, rowKey, onRowClick } = this.props;
    return (
      <div className="table-container">
        <table className="table">
          <thead>
            <tr>
              {columns.map((col) => (
                <th key={col.dataIndex}>{col.title}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {datas.map((data) => (
              <tr
                key={data[rowKey]}
                onClick={() => onRowClick?.(data)}
              >
                {columns.map((col) => (
                  <td key={col.dataIndex}>
                    {col.render
                      ? col.render(data[col.dataIndex], data)
                      : data[col.dataIndex]}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }
}

5. Modal 공통 컴포넌트

jsx
// components/Modal.js
export default class Modal extends React.Component {
  render() {
    const { open, title, children, onClose, onConfirm } = this.props;
    if (!open) return null;

    return (
      <div className="modal-container">
        <div className="modal-dim" onClick={onClose} />
        <div className="modal-content">
          <div className="modal-close" onClick={onClose}>x</div>
          <div className="modal-title">{title}</div>
          <div className="modal-body">{children}</div>
          <div className="modal-footer">
            {onConfirm && (
              <Button buttonType="confirm" onClick={onConfirm}>확인</Button>
            )}
            <Button buttonType="cancel" onClick={onClose}>닫기</Button>
          </div>
        </div>
      </div>
    );
  }
}

6. SNS 컴포넌트

외부 API로부터 데이터를 불러와 표시하는 컴포넌트다.

jsx
// components/SNS.js
export default class SNS extends React.Component {
  state = { posts: [], selectedPost: undefined };

  componentDidMount() {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((res) => res.json())
      .then((posts) => this.setState({ posts }));
  }

  render() {
    const { posts, selectedPost } = this.state;
    const columns = [
      { dataIndex: "userId", title: "게시자" },
      { dataIndex: "title", title: "제목" },
      { dataIndex: "body", title: "내용" },
    ];

    return (
      <>
        <Table
          columns={columns}
          datas={posts}
          rowKey="id"
          onRowClick={(post) => this.setState({ selectedPost: post })}
        />
        <Modal
          open={!!selectedPost}
          title="게시물 상세"
          onClose={() => this.setState({ selectedPost: undefined })}
        >
          <p>제목: {selectedPost?.title}</p>
          <p>내용: {selectedPost?.body}</p>
        </Modal>
      </>
    );
  }
}

7. Custom Event 기반 Export

webpack 설정 없이 마이크로앱을 Export하기 위해 custom event와 window 객체를 사용한다.

javascript
// legacy/index.js
import App from "./App";
import SNS from "./components/SNS";

// 마이크로앱 인터페이스 구성
const renderApp = (container, props) => {
  const root = ReactDOM.createRoot(container);
  root.render(<App baseName={props?.baseName} />);
  return () => root.unmount();
};

const renderSNS = (container, props) => {
  const root = ReactDOM.createRoot(container);
  root.render(<SNS {...props} />);
  return () => root.unmount();
};

// window 객체에 등록
if (!window.legacy) {
  window.legacy = {
    default: { render: renderApp },
    SNS: { render: renderSNS },
  };
}

// Custom Event로 초기화 알림
document.dispatchEvent(
  new CustomEvent("legacyInit", {
    detail: window.legacy,
  })
);

8. Routing과 Header 컴포넌트

jsx
// components/Header.js
export default class Header extends React.Component {
  state = { selected: "/" };

  render() {
    const items = [
      { key: "/", label: "SNS" },
      { key: "/mail-list", label: "메일 목록" },
      { key: "/shopping-list", label: "쇼핑 목록" },
    ];

    return (
      <nav className="header">
        {items.map((item) => (
          <a
            key={item.key}
            href={item.key}
            className={`header-box ${
              this.state.selected === item.key ? "selected" : ""
            }`}
            onClick={(e) => {
              e.preventDefault();
              this.setState({ selected: item.key });
              // react-router-dom의 navigate 또는 window.history.pushState
            &#125;&#125;
          >
            {item.label}
          </a>
        ))}
      </nav>
    );
  }
}

핵심 정리

  1. webpack 설정 없는 CRA: 레거시 프로젝트는 config-overrides.js 없이 순수 CRA로 운영하므로 toString-loader, UMD Export 등을 사용할 수 없다
  2. 클래스 컴포넌트: 레거시 호환성을 위해 모든 컴포넌트를 클래스 컴포넌트로 작성하되, API 인터페이스는 docs/web과 일관성을 유지한다
  3. Custom Event Export: webpack UMD 대신 custom event와 window 객체를 통해 마이크로앱 인터페이스를 제공한다
  4. 동일한 공유 컴포넌트 API: Button, Table, Modal이 docs/web과 동일한 props 구조를 가지므로 소비하는 측에서 일관된 방식으로 사용할 수 있다

다음 단계

크로스 프로젝트 통합 ->