상태 관리 라이브러리 - MobX (3. 응용)

eeensu·2023년 8월 6일
0
post-thumbnail

이제 다음과 같이 조금 더 복잡한 observer store를 만들어보겠다. 이번 예제에서는 TodoList에 대해서, 할일 목록들의 CRUD(Create / Read / Update / Delete)를 할 수 있도록 하였다. 또한 수행 여부와 다음 할일을 가르쳐주고, 총 몇개를 완료했는지 를 알려주는 등의 기능도 추가해보았다.

  1. 먼저 ObservableTodoStore 를 작성해준다.
// Todo 타입을 지정해준다
export interface Todo {
    task: string;								// 할일
    completed: boolean;							// 수행여부
}

export class ObservableTodoStore {
    todos: Todo[] = [];							// Todo를 담은 배열, 기본적으로 관찰할 state

    constructor() {
        makeObservable(this, {
            todos: observable,					
            completedTodosCount: computed,		
            report: computed,
            addTodo: action,
            deleteTodo: action
        });
     
      	// state가 업데이트 될 때마다 자동으로 실행
        autorun(() => console.log(this.report));
    }
	
  	// 완료한 todo의 개수를 리턴하는 computed value
    public get completedTodosCount(): number {
        return this.todos.filter(todo => todo.completed).length;
    }
	
  	// 다음에 할 todo의 이름과 현재 진행여부를 알려주는 computed value
    public get report(): string {
        if (this.todos.length === 0) {
            return 'None';
        }

        const nextTodo = this.todos.find(todo => !todo.completed);
        return `Next todo: "${nextTodo ? nextTodo.task : '<none>'}".
                Progress: ${this.completedTodosCount} / ${this.todos.length}`;
    }
	
  	// todos 배열에 새로운 할일을 추가해주는 action
    public addTodo(task: string): void {
        this.todos.push({
            task: task,
            completed: false,
        });
    };
  	
	// todos 배열에서 인덱스를 받아와 todo를 삭제해주는 action
    public deleteTodo(index: number) {
        const newTodo = this.todos.filter((_, i) => i !== index);
        this.todos = newTodo;
    }
}

// 인스턴스를 생성하여 export
export const observableTodoStore = new ObservableTodoStore();

  1. observer store를 구독하는 컴포넌트 작성 (부모 컴포넌트)
const TodoList: FC = () => {
    const store = observableTodoStore;				// observer 클래스 구독

    const onNewTodo = () => {
     	// prompt 를 활용해 집적 입력하여 새 todo를 추가해줌
        const select = prompt('Enter a new todo', 'coffee plz') as string;	
        store.addTodo(select);
    };

    return (
        <div>
            <h2>오늘 나의 할일 목록!</h2>
            <button onClick={onNewTodo}>New Todo!</button> 
            <br />

            {store.report}
            <br />

            <ul>
              	// 각 할일의 목록을 나타내는 li 컴포넌트이다.
              	// delete 함수를 위해 인자로 요소의 인덱스를 전달해줌.
                {store.todos.map((todo, i) => (
                    <TodoView key={todo.task} todo={todo} index={i}/>
                ))}
            </ul>       
        </div>
    );
};

export default observer(TodoList);

  1. 자식 컴포넌트인 <TodoView /> 작성
// 부모로 부터 받는 Props 타입 작성
interface Props {
    todo: Todo; 
    index: number;
}

const TodoView: FC<Props> = ({ todo, index }) => {    

  	// delete action을 호출하기위해 store를 가져옴
    const store = observableTodoStore;
  	
  	// 완료 여부를 컨트롤하는 함수
    const onToggleCompleted = () => {
        todo.completed = !todo.completed;
    };
	
  	// 할일의 이름을 변경하는 propmt 생성
    const onRename = () => {
        const rename = prompt(`prev task name : ${todo.task}`, 'input new task name') as string;
        todo.task = rename || todo.task;
    };
  
    // index를 받아온 props를 삭제함수의 인자로 보내줌. 
    const onDelete = () => {
        store.deleteTodo(index);
    };

    return (
        <li>
            <label style={{ marginRight: '1rem' }}>
                <input 
                    type='checkbox'
                    checked={todo.completed}
                    onChange={onToggleCompleted}
                />
                {todo.task}
            </label>               
            <button onClick={onRename} style={{ marginRight: '1rem' }}>Edit task name</button>
            <button onClick={onDelete}>Delete task</button>
        </li>
    );
};

export default observer(TodoView);


결과 확인




mobx에서 권장하는 react 최적화법

  1. 많은 소형 컴포넌트를 사용해라
    observer component는 모든 값을 추적하고 변경사항이 있으면 다시 렌더링한다. 따라서 컴포넌트가 작을수록 다시 렌더링해야 하는 변경사항이 적다. 컴포넌트를 작게 만드는 것은 UI의 더많은 부분이 서로 독립적으로 렌더링할 가능성이 있음을 의미하기 때문이다.


  2. value에 대한 참조를 최대한 크게해라
    slower - <DisplayName name={persone.name} age={person.age}/>
    faster - <DisplayName person={person} />

    slower는 name과 age 속성에 의존성을 가지며, 부모 컴포넌트가 리렌더링될 때마다 DisplayName 컴포넌트도 리렌더링될 가능성이 높다.

    반면, fater는 리렌더링될 때 person 객체의 레퍼런스가 변경되지 않는 한, DisplayName 컴포넌트는 person 객체의 속성 변화를 감지하지 않아도 된다. 만약 소유 컴포넌트가 충분히 빠르다면 사용해도 문제는 없다.


  3. 렌더링할 때 데이터를 반환하는 컴포넌트를 사용해라
    const GenericNameDisplayer = observer(({ getName }) => <DisplayName name={getName()} />

    observer를 컴포넌트에 씌워주면 MobX 관찰 가능한 상태를 읽고 사용할 때, 해당 상태에 대한 의존성을 자동으로 추적하게 된다.

profile
안녕하세요! 26살 프론트엔드 개발자입니다! (2024/03 ~)

0개의 댓글