테마
레거시 프로젝트 구성 - CRA 기반 컴포넌트 구현
Create React App 기반 레거시 프로젝트를 구축하고 webpack 설정 없이 Button, Table, Modal, SNS, Routing, Header 컴포넌트를 구현한다
학습 목표
- webpack 설정 없는 CRA 레거시 프로젝트의 특징과 제약을 이해한다
- 클래스 컴포넌트 기반의 공통 컴포넌트(Button, Table, Modal)를 구현할 수 있다
- SNS 컴포넌트와 라우팅/헤더를 작성할 수 있다
- 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
}}
>
{item.label}
</a>
))}
</nav>
);
}
}핵심 정리
- webpack 설정 없는 CRA: 레거시 프로젝트는 config-overrides.js 없이 순수 CRA로 운영하므로 toString-loader, UMD Export 등을 사용할 수 없다
- 클래스 컴포넌트: 레거시 호환성을 위해 모든 컴포넌트를 클래스 컴포넌트로 작성하되, API 인터페이스는 docs/web과 일관성을 유지한다
- Custom Event Export: webpack UMD 대신 custom event와 window 객체를 통해 마이크로앱 인터페이스를 제공한다
- 동일한 공유 컴포넌트 API: Button, Table, Modal이 docs/web과 동일한 props 구조를 가지므로 소비하는 측에서 일관된 방식으로 사용할 수 있다