파일트리 예시
user
- container
UserListContainer.tsx
- model1
UserApiModel.ts
UserModel.ts
- repository
UserRepository.ts
- store(service)
UserStore.ts
- view
UserEditFormView.tsx
UserListTableView.tsx
UserListView.tsx
환경구축
yarn create react-app todo
cd todo
yarn add --dev customize-cra react-app-rewired @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties core-decorators mobx mobx-react autobind-decorator
package.json의 script수정
package.json에 있는 scripts 중, eject에 있는 것을 제외한 모든 react-scripts 를 react-app-rewired 로 교체한다따로 설정해 놓으신 스크립트가 있어도 react-scripts 만 교체한다
...
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject",
...
}
...
package.json에 babel 세팅을 추가
...
"babel": {
"presets": [
"react-app"
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
]
]
}
...
프로젝트 파일 루트 폴더에 config-overrides.js를 추가
const {
addDecoratorsLegacy,
disableEsLint,
override,
} = require("customize-cra");
module.exports = {
webpack: override(
disableEsLint(),
addDecoratorsLegacy()
),
};
tsconfig.json (혹은 jsconfig.json)에서 컴파일 옵션을 추가
{
"compilerOptions": {
...
"experimentalDecorators": true
...
},
...
}
필요 폴더및 파일생성
cd src
mkdir containers stores views
cd containers
touch TodoEditFormContainer.js SearchbarContainer.js TodoListContainer.js
cd stores
touch TodoStore.js
cd views
touch TodoEditFormView.js TodoListView.js
src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "mobx-react";
import todoStore from "./stores/TodoStore";
ReactDOM.render(
<Provider todoStore={todoStore}>
<App />
</Provider>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
src/App.js
import React, { Component } from "react";
import SearchbarContainer from "./containers/SearchbarContainer";
import TodoEditFormContainer from "./containers/TodoEditFormContainer";
import TodoListContainer from "./containers/TodoListContainer";
export default class App extends Component {
render() {
return (
<div>
<TodoEditFormContainer />
<SearchbarContainer />
<TodoListContainer />
</div>
);
}
}
src/containers/TodoEditFormContainer.js
import React, { Component } from "react";
import TodoEditFormView from "../views/TodoEditFormView";
import { inject, observer } from "mobx-react";
import autobind from "autobind-decorator";
import generateId from "../IDGenerator";
// autobind는 inject밑에 배치해야한다
@inject("todoStore")
@autobind
@observer
class TodoEditFormContainer extends Component {
onSetTodoProps(name, value) {
this.props.todoStore.setTodoProps(name, value);
}
// generateId 랜덤한 id를 생성
onAddTodo() {
let { todo } = this.props.todoStore;
todo = { ...todo, id: generateId(5) };
this.props.todoStore.addTodo(todo);
}
onUpdateTodo() {
this.props.todoStore.updateTodo();
}
onRemoveTodo() {
this.props.todoStore.removeTodo();
}
render() {
let { todoStore } = this.props;
return (
<TodoEditFormView
todo={todoStore.todo}
onSetTodoProps={this.onSetTodoProps}
onAddTodo={this.onAddTodo}
onUpdateTodo={this.onUpdateTodo}
onRemoveTodo={this.onRemoveTodo}
/>
);
}
}
export default TodoEditFormContainer;
src/IDGenerator.js
function generateId(length) {
let chars =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz".split("");
if (!length) {
length = Math.floor(Math.random() * chars.length);
}
let str = "";
for (let i = 0; i < length; i++) {
str += chars[Math.floor(Math.random() * chars.length)];
}
return str;
}
export default generateId;
export { generateId };
src/containers/TodoListContainer.js
import autobind from "autobind-decorator";
import React, { Component } from "react";
import TodoListView from "../views/TodoListView";
import { inject, observer } from "mobx-react";
@inject("todoStore")
@autobind
@observer
class TodoListContainer extends Component {
onSelectedTodo(todo) {
this.props.todoStore.selectedTodo(todo);
}
render() {
let { todos, searchText } = this.props.todoStore;
todos = todos.filter(
(todo) =>
todo.title.toLowerCase().indexOf(searchText.toLowerCase()) !== -1
);
return <TodoListView todos={todos} onSelectedTodo={this.onSelectedTodo} />;
}
}
export default TodoListContainer;
src/containers/SearchContainer.js
import React, { Component } from "react";
import styles from "../views/style.module.css";
import { inject, observer } from "mobx-react";
import autobind from "autobind-decorator";
@inject("todoStore")
@autobind
@observer
class SearchbarContainer extends Component {
onChangeSearchText(searchText) {
this.props.todoStore.setSearchText(searchText);
}
render() {
return (
<div className={styles.search}>
<input
type="text"
onChange={(e) => this.onChangeSearchText(e.target.value)}
/>
</div>
);
}
}
export default SearchbarContainer;
src/views/TodoEditFormView.js
import React, { Component } from "react";
import styles from "./style.module.css";
export default class TodoEditFormView extends Component {
render() {
const { todo, onSetTodoProps, onAddTodo, onUpdateTodo, onRemoveTodo } =
this.props;
return (
<div>
<input
type="text"
onChange={(e) => onSetTodoProps("title", e.target.value)}
value={todo && todo.title ? todo.title : ""}
/>
<button className={styles.btn} onClick={onAddTodo}>
등록
</button>
<button className={styles.btn} onClick={onUpdateTodo}>
업데이트
</button>
<button className={styles.btn} onClick={onRemoveTodo}>
삭제
</button>
</div>
);
}
}
src/views/TodoListView.js
import { observer } from "mobx-react";
import React, { Component } from "react";
import styles from "./style.module.css";
@observer
class TodoListView extends Component {
render() {
const { todos, onSelectedTodo } = this.props;
// Array.isArray() 메서드는 인자가 Array인지 판별합니다.
return (
<div className={styles.listWrap}>
<ul>
{Array.isArray(todos) && todos.length ? (
todos.map((todo) => (
<li
className={styles.list}
key={todo.id}
onClick={() => onSelectedTodo(todo)}
>
{todo.title}
</li>
))
) : (
<li>empty</li>
)}
</ul>
</div>
);
}
}
export default TodoListView;
src/views.style.module.css
.btn {
margin-left: 10px;
}
.search {
margin-top: 10px;
}
.listWrap {
margin-top: 20px;
}
.list {
margin-bottom: 10px;
width: 100%;
}
.list:hover {
background-color: aliceblue;
}
src/stores/TodoStore.js
import { observable, action, makeObservable, toJS, computed } from "mobx";
class TodoStore {
// mobX6 부터 makeObservable 함수를 통해 생성자에서 makeObservable를 지정해줘야 데이터 변경이 반영된다.
constructor() {
makeObservable(this);
}
// 입력투두리스트
@observable
_todo = {}; // id, title, date
// 생성된 투두리스트
@observable
_todos = [];
@observable
_searchText = "";
get todo() {
return this._todo;
}
// this._todos ? this._todos.slice() : [];
// observervable로 관리하는 데이터들은, mobx가 정의한 observervable데이터로 랩핑이된다.
// computed 를 사용하게 되면, tods가 호출될때마다 toJS를 계속해서 호출될텐데, computed를사용시에, observervable데이터가 변경이 일어나지 않으면 최종으로 캐싱하고 있는 데이터를 리턴한다.
// get메소드에서 observervable데이터에 대한 특정연산이 진행될때는 꼭 computed사용해야한다.
@computed
get todos() {
return toJS(this._todos);
}
get searchText() {
return this._searchText;
}
// todo 설정
@action
setTodoProps(name, value) {
this._todo = {
...this._todo,
[name]: value,
};
}
@action
addTodo(todo) {
this._todos.push(todo);
}
@action
selectedTodo(todo) {
this._todo = todo;
}
@action
updateTodo() {
let foundTodo = this._todos.find((todo) => todo.id === this._todo.id);
foundTodo.title = this._todo.title;
this._todo = {};
}
@action
removeTodo() {
let index = this._todos.findIndex((todo) => todo.id === this._todo.id);
if (index > -1) {
this._todos.splice(index, 1);
console.log(toJS(this._todos));
}
this._todo = {};
}
@action
setSearchText(searchText) {
this._searchText = searchText;
}
}
// 일반 자바스크립트 객체이기에 new 로 객체생성후 export한다.
export default new TodoStore();