비트 컴퓨터 [React] - ⑩
1. 실습 ①
Todo
import { observer } from "mobx-react-lite";
const { makeObservable, observable, computed, action, autorun } = require("mobx");
class ObservableTodoStore {
todos = [];
pendingRequests = 0;
constructor() {
makeObservable(this, {
todos: observable,
pendingRequests: observable,
completedTodosCount: computed,
report: computed,
addTodo: action,
});
autorun(() => console.log(this.report));
}
get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
get report() {
if (this.todos.length === 0)
return "<none>";
const nextTodo = this.todos.find(todo => todo.completed === false);
return `Next todo: "${nextTodo ? nextTodo.task : "<none>"}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
const observableTodoStore = new ObservableTodoStore();
observableTodoStore.addTodo("read MobX tutorial");
observableTodoStore.addTodo("try MobX");
observableTodoStore.todos[0].completed = true;
observableTodoStore.todos[1].task = "try MobX in own project";
observableTodoStore.todos[0].task = "grok MobX tutorial";
const TodoList = observer(({store}) => {
const onNewTodo = () => {
store.addTodo(prompt('Enter a new todo:','coffee plz'));
}
return (
<div>
{ store.report }
<ul>
{ store.todos.map(
(todo, idx) => <TodoView todo={ todo } key={ idx } />
) }
</ul>
{ store.pendingRequests > 0 ? <marquee>Loading...</marquee> : null }
<button onClick={ onNewTodo }>New Todo</button>
<small> (double-click a todo to edit)</small>
</div>
);
})
const TodoView = observer(({todo}) => {
const onToggleCompleted = () => {
todo.completed = !todo.completed;
}
const onRename = () => {
todo.task = prompt('Task name', todo.task) || todo.task;
}
return (
<li onDoubleClick={ onRename }>
<input
type='checkbox'
checked={ todo.completed }
onChange={ onToggleCompleted }
/>
{ todo.task }
{ todo.assignee
? <small>{ todo.assignee.name }</small>
: null
}
</li>
);
})
function App() {
return (
<div>
<TodoList store={observableTodoStore}></TodoList>
</div>
);
}
export default App;
수정
import './App.css';
import { observer } from "mobx-react-lite";
const { makeObservable, observable, computed, action, autorun } = require("mobx");
class ObservableTodoStore {
todos = [];
pendingRequests = 0;
constructor() {
makeObservable(this, {
todos: observable,
pendingRequests: observable,
completedTodosCount: computed,
report: computed,
addTodo: action,
});
autorun(() => console.log(this.report));
}
get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
get report() {
if (this.todos.length === 0)
return "<none>";
const nextTodo = this.todos.find(todo => todo.completed === false);
return <h2><span>다음 할 일: </span>{nextTodo ? nextTodo.task : `수고했어!\n`} <br/>
<span>진행 상황: </span>{this.completedTodosCount} / {this.todos.length}</h2>
}
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
const observableTodoStore = new ObservableTodoStore();
observableTodoStore.addTodo("read MobX tutorial");
observableTodoStore.addTodo("try MobX");
observableTodoStore.todos[0].completed = true;
observableTodoStore.todos[0].task = "커피 사기";
observableTodoStore.todos[1].task = "뚜비 놀아주기";
const TodoList = observer(({store}) => {
const onNewTodo = () => {
store.addTodo(prompt('Enter a new todo:','coffee plzzz....☕'));
}
return (
<div>
{ store.report }
<ul>
{ store.todos.map(
(todo, idx) => <TodoView todo={ todo } key={ idx } />
) }
</ul>
{ store.pendingRequests > 0 ? <marquee>Loading...</marquee> : null }
<button onClick={ onNewTodo }>New Todo</button>
<bold> (double-click a todo to edit)</bold>
</div>
);
})
const TodoView = observer(({todo}) => {
const onToggleCompleted = () => {
todo.completed = !todo.completed;
}
const onRename = () => {
todo.task = prompt('Task name', todo.task) || todo.task;
}
return (
<li onDoubleClick={ onRename }>
<input
type='checkbox'
checked={ todo.completed }
onChange={ onToggleCompleted }
/>
{ todo.task }
{ todo.assignee
? <small>{ todo.assignee.name }</small>
: null
}
</li>
);
})
function App() {
return (
<div>
<TodoList store={observableTodoStore}></TodoList>
</div>
);
}
export default App;
- App.css
@font-face {
font-family: 'KOHIBaeumOTF';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/[email protected]/KOHIBaeumOTF.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'GmarketSansMedium';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/[email protected]/GmarketSansMedium.woff') format('woff');
font-weight: normal;
font-style: normal;
}
*{
list-style:none;
font-family: 'GmarketSansMedium';
/* font-family: 'Yeongdo-Rg'; */
}
html{
background-color: #EFFFFD;
}
h2{
font-size: 1rem;
border-radius: 25px;
color: white;
background-color: #85F4FF;
padding: 10px;
}
h2 span{
color: #0043b0;
}
div{
border-radius: 25px;
background-color: #B8FFF9;
color: rgb(255,255,255);
padding: 40px;
text-align: center;
}
/*
h1{
font-family: 'KOHIBaeumOTF';
border-radius: 30px;
color: #064420;
background-color: #E4EFE7;
padding: 10px;
text-align: center;
} */
button {
background: none;
border: 3px solid #fff;
border-radius: 5px;
color: #fff;
display: block;
font-size: 1.6em;
font-weight: bold;
margin: 1em auto;
padding: 1em 2em;
position: relative;
text-transform: uppercase;
}
button::before,
button::after {
background: #fff;
content: '';
position: absolute;
z-index: -1;
}
button:hover {
color: #0043b0;
}
실행 흐름
2. 실습 ②
ex1)
//1.
class Animal {
energyLevel;
constructor(name) {
this.energyLevel = 100;
makeAutoObservable(this);
}
reduceEnergy() {
this.energyLevel -= 10;
}
get isHungry() {
return this.energyLevel < 50;
}
}
const giraffe = new Animal();
autorun(() => {
console.log("Energy level:", giraffe.energyLevel)
})
autorun(() => {
if (giraffe.isHungry) {
console.log("Now I'm hungry!");
} else {
console.log("I'm not hungry!");
}
})
console.log("Now let's change state!");
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy();
}
function App() {
return (
<div className="App">
</div>
);
}
export default App;
ex2)
//2.
class Animal {
energyLevel;
constructor() {
this.energyLevel = 100;
// makeAutoObservable(this);
makeObservable(
this,
{
energyLevel:observable,
isHungry:computed,
reduceEnergy: action,
}
);
}
reduceEnergy() {
this.energyLevel -= 10;
}
get isHungry() {
return this.energyLevel < 50;
}
}
const giraffe = new Animal();
autorun(() => {
console.log("Energy level:", giraffe.energyLevel)
})
autorun(() => {
if (giraffe.isHungry) {
console.log("Now I'm hungry!");
} else {
console.log("I'm not hungry!");
}
})
console.log("Now let's change state!");
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy();
}
function App() {
return (
<div className="App">
</div>
);
}
export default App;
ex3)
//3.
class Animal {
energyLevel;
constructor() {
this.energyLevel = 100;
// makeAutoObservable(this);
makeObservable(
this,
{
energyLevel:observable,
isHungry:computed,
reduceEnergy: action,
}
);
autorun(() => {
console.log("Energy level:", this.energyLevel)
})
autorun(() => {
if (this.isHungry) {
console.log("Now I'm hungry!");
} else {
console.log("I'm not hungry!");
}
})
}
reduceEnergy() {
this.energyLevel -= 10;
}
get isHungry() {
return this.energyLevel < 50;
}
}
const giraffe = new Animal();
console.log("Now let's change state!");
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy();
}
function App() {
return (
<div className="App">
</div>
);
}
export default App;
ex4)
autorun
은 객체가 생성될 때 자동으로 실행된다.
-
observer 변수가 값이 갱신이 일어나면 autorun이 호출된다.
-
observer 변수를 사용하는 autorun이 호출된다.
-
함수가 observer 변수를 사용하고 있고, observer 변수를 갱신이 일어나면 자동 호출된다.
- 옵저버 변수 갱신시에 옵저버 변수를 사용하는
autorun
이나,compute
자동 호출compute
변수 갱신 시에compute
변수를 사용하는autorun
호출
[참고] get, set
class Person{ constructor(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; } //getter get fullName(){ console.log('---getter 호출---'); return `${this.firstName} ${this.lastName}`; } //setter set fullName(name){ console.log('***setter 호출***'); [this.firstName, this.lastName] = name.split(' ') //공백으로 분리 시키기 } }
import { action, autorun, computed, makeAutoObservable, makeObservable, observable } from 'mobx';
import './App.css';
//4.
class Animal {
energyLevel;
constructor() {
this.energyLevel = 100;
// makeAutoObservable(this);
makeObservable(
this,
{
//mobx가 energyLevel를 항상 주시하세요
energyLevel:observable,
//computed가 함수이냐, 변수이냐 -> 변수이다!
//boolean 타입의 변수로 사용되고 있다.
isHungry:computed,
//energyLevel의 값의 변화를 주는 함수
reduceEnergy: action,
}
);
//energyLevel의 상태가 변화하면 호출된다.
autorun(() => {
console.log("Energy level1:", this.energyLevel)
})
autorun(() => {
console.log("Energy level2:", this.energyLevel)
})
//isHungry에 변화가 감지되면 호출된다.
//false -> true / true -> false
autorun(() => {
console.log(this.isHungry ? "배고파" : "배고프지 않아");
/*
if (this.isHungry) {
console.log("배고파!");
} else {
console.log("배고프지 않아!");
}*/
})
}
//옵저버 변수가 값이 갱신이 일어나면 autorun이 호출된다.
// 옵저버 변수를 사용하는 autorun이 호출된다.
reduceEnergy() {
console.log('에너지 줄임');
this.energyLevel -= 10;
}
//isHungry => 리턴 결과
//이 함수가 옵저버 변수를 사용하고 있고,
// 옵저버 변수가 갱신이 일어나면 자동 호출된다.
get isHungry() {
console.log('isHungry');
return this.energyLevel < 95;
// return 100; //옵저버 함수를 사용하지 않는 경우
}
}
const giraffe = new Animal();
console.log("-----change state!-----");
for (let i = 0; i < 2; i++) {
giraffe.reduceEnergy();
}
function App() {
return (
<div className="App">
</div>
);
}
export default App;
✅ computed
-
computed 함수는 연산된 값을 사용해야 할 때 사용됩니다.
-
이 값을 조회할 때 마다 특정 작업을 처리하는것이 아니라, 이 값에서 의존하는 값이 바뀔 때 미리 값을 계산해놓고 조회 할 때는 캐싱된 데이터를 사용한다
- computed가 observe 하던 값의 변화에 따라 결과를 저장해 cache에 저장 reaction의 parameter로 전달된 함수 실행
import { observable, reaction, computed, autorun } from 'mobx';
// Observable State 만들기
const calculator = observable({
a: 1,
b: 2
});
// **** 특정 값이 바뀔 때 특정 작업 하기!
reaction(
() => calculator.a,
(value, reaction) => {
console.log(`a 값이 ${value} 로 바뀌었네요!`);
}
);
reaction(
() => calculator.b,
value => {
console.log(`b 값이 ${value} 로 바뀌었네요!`);
}
);
// **** computed 로 특정 값 캐싱
const sum = computed(() => {
console.log('계산중이예요!');
return calculator.a + calculator.b;
});
sum.observe(() => calculator.a); // a 값을 주시
sum.observe(() => calculator.b); // b 값을 주시
calculator.a = 10;
calculator.b = 20;
//**** 여러번 조회해도 computed 안의 함수를 다시 호출하지 않지만..
console.log(sum.value);
console.log(sum.value);
// 내부의 값이 바뀌면 다시 호출 함
calculator.a = 20;
console.log(sum.value);
계산중이예요!
a 값이 10 로 바뀌었네요!
계산중이예요!
b 값이 20 로 바뀌었네요!
계산중이예요!
30
30
a 값이 20 로 바뀌었네요!
계산중이예요!
40
✅ makeObservable
-
MobX 6버전 (2020년 9월 업데이트) 에서 부터 데코레이터 사용하지 않고, makeObservable을 사용하는것으로 권장하는것으로 바뀌었다.
-
예전 버전의 mobx에서는 decorate 사용, 최신버전에서 makeObservable 사용
import { action, makeObservable, observable } from 'mobx'
class Count {
number: number = 0
constructor() {
makeObservable(this, {
number: observable,
increase: action,
decrease: action,
})
}
increase = () => {
this.number++
}
decrease = () => {
this.number--
}
}
const countStore = new Count()
export default countStore
import React, { Component } from "react";
import { makeObservable, observable, action } from "mobx";
import { observer } from "mobx-react";
class Counter extends Component {
number = 0;
constructor() {
super();
makeObservable(this, { // 예전 버전의 mobx에서는 decorate 사용, 최신버전에서 makeObservable사용
number: observable, // observable이니 내가 관찰할 상태
increase: action, // action 이니 상태의 변화를 일으킬 친구
decrease: action, // action 이니 상태의 변화를 일으킬 친구
});
}
increase = () => {
this.number++;
};
decrease = () => {
this.number--;
};
render() {
return (
<div>
<h1>{this.number}</h1>
<button onClick={this.increase}>+1</button>
<button onClick={this.decrease}>-1</button>
</div>
);
}
}
export default observer(Counter);
// observer로 감싸줌으로써 이 Counter 클래스는 mobx가 @observable로 지정된 state를 적절히 rerendering 시켜주는 역할을 하게된다.
마지막의 export default observer(Counter) 를 통해서
observable값이 변할때 forceUpdate를 호출함으로서 자동으로 변화를 감지해 화면에 반영되는 것입니다.
- setState이런거 안해줘도 됩니다
✅ makeAutoObservable
- super나 subclassed에서는 사용이 불가하기 때문에 makeAutoObservable 방식도 알아두어야 한다.
import { makeAutoObservable } from 'mobx'
class Count {
number: number = 0
constructor() {
makeAutoObservable(this)
}
increase = () => {
this.number++
}
decrease = () => {
this.number--
}
}
const countStore = new Count()
export default countStore
✅ observable
import { observable, reaction, computed, autorun } from 'mobx';
// **** Observable State 만들기
const calculator = observable({
a: 1,
b: 2
});
- 이렇게 Observable State 를 만들고나면 MobX 가 이 객체를 "관찰 할 수" 있어서 변화가 일어나면 바로 탐지해낼수있습니다.
- object로 만들면 코드가 더 줄어든다. observable로 감싸주기만 하면 된다.
import { observable } from 'mobx'
const countObject = observable({
// 헷갈릴 수 있으니 num으로 작명
num: 0,
increase() {
this.num++
},
decrease() {
this.num--
},
})
export default countObject
✅ observe
- observe함수는 관찰 가능 항목의 변화를 감시하는 데 사용됩니다.
- 형식
observe(target, propertyName?, listener, invokeImmediately?)
-
propertyName관찰 할 속성을 지정하는 선택적 매개 변수입니다.
-
observe(user.name, listener)
과 다릅니다observe(user, "name", listener)
- 첫 번째는 전류
value
를user.name
관찰하고 후자name
은 사용자의 속성을 관찰합니다 .
- 첫 번째는 전류
-
-
listenerObservable
이 변경 될 때마다 호출되는 콜백입니다.newValue
및oldValue
매개 변수를 사용하여 리스너를 호출하는boxed Observable
을 제외하고 변형을 설명하는 단일 변경 객체를 수신합니다 .
import { observable, observe } from "mobx";
const person = observable({
firstName: "John",
lastName: "Smith"
});
const disposer = observe(person, change => {
console.log(
`${change.type} ${change.name} from ${change.oldValue} to ${
change.object[change.name]
}`
);
});
person.firstName = "Jane";
✅ observer
-
상단에 MobX 관련 기능을 선언한 후, 코드 하단 쪽에 decorate함수를 호출한 부분이 있다.
-
해당 decorate가 Counter 컴포넌트에게 상태변환을 할 수 있도록 도와주는 함수이다.
-
마치 리덕스에서 Connect함수의 인자로 mapStateToProps, mapDispatchToProps를 넣는 것과 유사한 형태이다.
-
-
마지막 줄의 observer는 observable로 선언한 상태 값(위 코드에서는 number 값)이 변할 때, 컴포넌트 API인 forceUpdate()를 자동 호출하여 변경된 값이 화면에 반영된다.
import React, { Component} from 'react';
import { decorate, observable, action } from 'mobx';
import { observer } from 'mobx-react';
class Counter extends Component {
number = 0;
increase = () => {
this.number++;
}
decrease = () => {
this.number--;
}
render() {
return (
<div>
<h1>{this.number}</h1>
<button onClick={this.increase}>+1</button>
<button onClick={this.decrease}>-1</button>
</div>
)
}
}
// 데코레이터 설정 부분
decorate(Counter ,{
number: 'observable',
increase: 'action',
decrease: 'action'
});
export default observer(Counter);
import React from 'react'
import { observer } from 'mobx-react'
import store from './store'
// 컴포넌트를 observer로 감싸주어 state가 실시간으로 변경되는 것을 감지한다
const App: React.FC = observer(() => {
const { countClass, countObject } = store
return (
<div style={{ padding: '50px' }}>
<div style={{ marginBottom: '50px' }}>
<h1>Count (Class)</h1>
<div>number: {countClass.number}</div>
<button onClick={() => countClass.increase()}>plus</button>
<button onClick={() => countClass.decrease()}>minus</button>
</div>
<div style={{ marginBottom: '50px' }}>
<h1>Count (Object)</h1>
<div>num: {countObject.num}</div>
<button onClick={() => countObject.increase()}>increment</button>
</div>
</div>
)
})
export default App
// export default observer(App) // 이렇게 감싸줄수도 있다
✅ autorun
- reaction의 중요성은 observable state에 대한 소비자(consumer)를 만들어내거나 무언가 관련된 요소가 바뀔 때 자동적으로 부수효과를 실행하는 데 있습니다.
- 형식
autorun(effect: (reaction) => void)
-
autorun
함수는 변화를 감지할 때마다 실행하는 함수 한 개를 수용하며,autorun
자체를 생성할 때도 한 번 실행됩니다. -
autorun
은observable
또는computed
로 주석 설정한 observable state의 변화에만 반응합니다.
-
트래킹 동작 원리
-
autorun은 반응형 컨텍스트 내의 effect를 실행함으로써 동작합니다.
-
제공된 함수가 실행되는 동안 MobX는 effect로부터 직접적으로 혹은 간접적으로 읽어들이는 모든 observable 및 computed 값을 트래킹 합니다.
-
함수 동작이 끝나면 MobX는 읽어들인 모든 observable 항목들을 모으고 구독하며, 해당 항목들 중 일부가 다시 변경될 때까지 기다립니다.
-
변경이 완료되면 autorun은 다시 트리거 되고 전체 과정을 반복합니다.
-
⚪ effect 함수 내부의 obsevable를 subscribe 합니다.
⚪ obsevable이 변경되면 autorun 를 실행시킵니다.
⚪ autorun은 초기화시 실행 됩니다.
import { makeAutoObservable, autorun } from "mobx"
class Animal {
name
energyLevel
constructor(name) {
this.name = name
this.energyLevel = 100
makeAutoObservable(this)
}
reduceEnergy() {
this.energyLevel -= 10
}
get isHungry() {
return this.energyLevel < 50
}
}
const giraffe = new Animal("Gary")
autorun(() => {
console.log("Energy level:", giraffe.energyLevel)
})
autorun(() => {
if (giraffe.isHungry) {
console.log("Now I'm hungry!")
} else {
console.log("I'm not hungry!")
}
})
console.log("Now let's change state!")
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy()
}
//1. initialize -> 2. read & subscripbe
Energy level: 100
I'm not hungry!
//3. update state
Now let's change state!
//4. notify and re-run autorun
Energy level: 90
Energy level: 80
Energy level: 70
Energy level: 60
Energy level: 50
Energy level: 40
Now I'm hungry!
Energy level: 30
Energy level: 20
Energy level: 10
Energy level: 0
-
위 코드의 처음 두 줄에 보이듯이, 두 개의 autorun 함수는 초기화될 때 한 번 실행됩니다.
- for 루프가 없어도 해당 두 줄은 보일 것입니다.
-
reduceEnergy action으로 energyLevel를 변경하기 위해 for 루프를 실행하면, autorun 함수가 observable state의 변화를 감지하는 '모든 순간' 새로운 로그를 출력합니다.
-
함수 "Energy level"의 측면에서 '모든 순간'이란 observable 속성을 가진 energyLevel이 변경되는 10회입니다.
-
함수 "Now I'm hungry"의 측면에서 '모든 순간'이란 computed 속성을 가진 isHungry가 변경되는 1회입니다.
⚪처음 초기화 시 실행 된다.
⚪인자로 받은 함수내에 있는 모든 observable를 추적한다.
- autorun 은 reaction 이나 computed 의 observe 대신에 사용 될 수 있는데, autorun 으로 전달해주는 함수에서 사용되는 값이 있으면 자동으로 그 값을 주시하여 그 값이 바뀔 때 마다 함수가 주시되도록 해줍니다. 여기서 만약에 computed 로 만든 값의 .get() 함수를 호출해주면, 하나하나 observe 해주지 않아도 됩니다.
import { observable, reaction, computed, autorun } from 'mobx';
// Observable State 만들기
const calculator = observable({
a: 1,
b: 2
});
// computed 로 특정 값 캐싱
const sum = computed(() => {
console.log('계산중이예요!');
return calculator.a + calculator.b;
});
// **** autorun 은 함수 내에서 조회하는 값을 자동으로 주시함
autorun(() => console.log(`a 값이 ${calculator.a} 로 바뀌었네요!`));
autorun(() => console.log(`b 값이 ${calculator.b} 로 바뀌었네요!`));
autorun(() => sum.get()); // sum
calculator.a = 10;
calculator.b = 20;
// 여러번 조회해도 computed 안의 함수를 다시 호출하지 않지만..
console.log(sum.value);
console.log(sum.value);
calculator.a = 20;
// 내부의 값이 바뀌면 다시 호출 함
console.log(sum.value);
a 값이 1 로 바뀌었네요!
b 값이 2 로 바뀌었네요!
계산중이예요!
a 값이 10 로 바뀌었네요!
계산중이예요!
b 값이 20 로 바뀌었네요!
계산중이예요!
30
30
a 값이 20 로 바뀌었네요!
계산중이예요!
40
✅ reaction
- 형식
reaction(() => value, (value, previousValue, reaction) => { sideEffect }, options?)
-
autorun 보다 더 세밀한 제어가 가능
-
2가지 인자를 받는데 첫번째 함수의 observable를 추적하고 renturn 값을 2번째 함수에서 사용합니다.
-
첫번째 함수에서 쓰인 observable의 변화에만 반응합니다.
-
autorun과 달리 초기화시 실행 되지 않습니다.
reaction(
() => giraffe.isHungry,
isHungry => {
if (isHungry) {
console.log("Now I'm hungry!");
} else {
console.log("I'm not hungry!");
}
console.log("Energy level:", giraffe.energyLevel);
}
);
Now let's change state!
Now I'm hungry!
Energy level: 40
⚪처음 초기화 시 실행 되지 않는다.
⚪effect 함수 내에서 쓰이는 giraffe.energyLevel의 변화는 추적하지 않는다.
Author And Source
이 문제에 관하여(비트 컴퓨터 [React] - ⑩), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@corone_hi/비트-컴퓨터-React-f1jqpbl5저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)