React - MobX part 2

이진화행·2022년 2월 8일
0

Parkjin

목록 보기
5/14

1. MobX의 적용

React와 MobX를 통한 UI를 위한 패키지구성 (기본)

  • container : React Component로 구성하며 store와 React Component를 연결하는 역할
  • view : 순수 React Component로 구성하며 container에 포함
  • repository(or api) : 서버와 통신을 담당하는 클래스로 구성
  • store(service) : 전역 state를 관리하는 Store 클래스로 구성
  • model : 서버의 model과 view model의 전환을 담당

파일트리 예시

user
	- container
    UserListContainer.tsx
    - model1
    UserApiModel.ts
    UserModel.ts
    - repository
    UserRepository.ts
    - store(service)
    UserStore.ts
    - view
    UserEditFormView.tsx
    UserListTableView.tsx
    UserListView.tsx

2. MobX의 예제 - Todo List

파일구조

1. 파일생성

환경구축

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

2. TodoEditForm

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();
profile
기술블로그

0개의 댓글