-
[React] RxJS 를 이용한 간단한 상태 관리 프로그램React 2019. 9. 27. 00:15
React 에서 RxJS 를 활용한 상태관리를 통하여 간단한 Todo List 를 만들어 봅니다.
RxJS 는 Reactive Programming 을 활용할때 사용할 수 있는 가장 좋은 라이브러리 중 하나 입니다.
국내에서는 RxJS 를 사용한 예제를 많이 찾아 볼 수가 없더군요.
익숙한 사람이 적은 것 일수도 있지만 굳이 도입 할 필요성을 못 느끼는 것 같기도 합니다.
Angular 를 주로 해왔던 전 어느정도 익숙 해 졌지만, 국내는 아직 React (또는 Vue) 가 강세고
Angular 를 잘 사용하지 않는 듯 합니다.
아무래도 진입장벽이 타 라이브러리에 비해 조금 높고, 그 중 RxJS, TypeScript 를 기본 베이스로 사용하는 부분이
크게 한 몫 하는 것 같습니다.
Angular 를 접해보지 않은 프론트엔드 개발자가 있다면 꼭 접해 보시길 바랍니다.
배울 점이 많은 프레임워크 입니다.
본론으로 들어가서 Redux (또는 MobX) 를 사용하지 않고 RxJS 의 Behavior Subject 를 활용하여
Stream 방식으로 간단한 Todo List 예제를 만들어 봅시다.
먼저 빈 프로젝트를 생성 합시다.
npx create-react-app react-rxjs — typescript
그리고 RxJS 를 추가합니다.
{ "name": "singleton", "version": "0.1.0", "private": true, "dependencies": { "@types/jest": "24.0.18", "@types/node": "12.7.5", "@types/react": "16.9.2", "@types/react-dom": "16.9.0", "react": "^16.9.0", "react-dom": "^16.9.0", "react-scripts": "3.1.1", "rxjs": "^6.5.3", "typescript": "3.6.3" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
먼저 TodoItem 의 모델을 정의합시다.
TodoItem 은 id, content, 그리고 해당 item 을 체크하는 isDone 으로 이루어진 간단한 모델 입니다.
export interface TodoItem { id: number; content: string; isDone: boolean; }
해당 TodoItem 들을 관리 할 서비스를 만듭시다.
이 서비스에는 TodoItem 의 상태 저장, 추가, 삭제, 토글 메소드 가 포함되어 있습니다.
import { BehaviorSubject, Observable } from 'rxjs'; import { TodoItem } from '../models'; class TodoServiceController { nextId = 3; // 임의의 초기 데이터 TodoInitData: TodoItem[] = [ { id: 1, content: 'Learn React', isDone: false }, { id: 2, content: 'Learn Angular', isDone: true } ]; private _data$: BehaviorSubject<TodoItem[]> = new BehaviorSubject<TodoItem[]>(this.TodoInitData); // _data$ Subject 에 직접 접근을 막기위해 임의의 Observable 로 변환합니다. readonly todoData$: Observable<TodoItem[]> = this._data$.asObservable(); // add Todo Data addTodo(content: string): void { this.TodoInitData = this.TodoInitData.concat({ content, id: this.nextId, isDone: false }); this._data$.next(this.TodoInitData); this.nextId++; } // delete Todo Data deleteTodo(id: number): void { this.TodoInitData = this.TodoInitData.filter(v => v.id !== id); this._data$.next(this.TodoInitData); } // Toggle Todo Data toggleIsDone(id: number, checked: boolean): void { this.TodoInitData = this.TodoInitData.map(v => ({ id: v.id, content: v.content, isDone: v.id === id ? checked : v.isDone })); this._data$.next(this.TodoInitData); } // 해당 State 가 필요 없을 때 실행 합니다. dispose(): void { this._data$.complete(); } } export const TodoService = new TodoServiceController();
Rxjs 를 활용한 상태관리의 핵심이 되는 서비스 입니다.
코드의 각 부분을 설명하면
- nextId 는 Todo 가 추가될때 들어 갈 임의의 Unique ID 입니다.
- TodoInitData 는 임의의 초기 데이터 입니다.
- _data$ 는 TodoItem 배열을 저장 할 Subject 로서 해당 Subject 로 TodoItems 의 데이터를 변경하고 방출 할 수 있습니다.
- todoData$ 는 위 만들어진 _data$ 를 단순 Observable 로 변경시킵니다.
- addTodo, deleteTodo, toggleIsDone 은 Todo 데이터의 조작을 담당하는 메소드 입니다.
- dispose 는 _data$ Subject 를 완료시켜서 연결된 스트림들에 대해 더이상 데이터를 방출하지 않도록 합니다. 이 과정에서 Subject 에 저장 된 데이터도 사라집니다.
RxJS 의 Subject 에 대하여 간단하게 설명하자면 Subject 는 Observable + Observer 의 특성을 가지고 있습니다.
데이터 구독이 가능하면서 동시에 데이터 변경도 가능합니다.
그리고 자체적으로 BroadCast 를 하기 때문에 여러개의 구독에 대하여 데이터를 방출 할 수 있습니다.
Subject 의 데이터 변경은 next 함수로 실행하게 되는데 next 함수에 새로운 데이터를 지정하면
최종값은 새롭게 지정된 값으로 바뀌게 되고 동시에 구독하고 있는 모든 Stream 에 해당 데이터를 방출 합니다.
Subject 종류는 여럿 있지만 그중 BehaviorSubject 는 마지막 값을 저장해두고
새로운 구독이 생기면 해당 값을 방출하는 특성이 있습니다.
간단하게 그림으로 살펴봅시다.
위 그림을 보면 보라색 구슬은 Subject 를 만들면서 던져주는 초기 데이터를 의미 합니다.
그후 붉은색, 초록색, 파란색 구슬은 해당 Stream 의 데이터를 의미하고 순서대로 데이터가 들어오는 것을 표시합니다.
next 메소드를 실행 한 순서라고 보시면 됩니다.
아래 2개의 라인은 해당 Subject 를 구독 하는 Stream 을 의미하며 첫 구독자가 구독을 시작했을때
초기 데이터인 보라색 구슬 데이터를 받고 그후 붉은색 구슬 데이터가 들어오면 붉은색 구슬 데이터를 받습니다.
이후 순서대로 들어오는 데이터를 받습니다.
두번째 Stream 은 중간에 구독을 시작하는데 그때 마지막으로 들어온 데이터인 초록색 데이터를 받는 것을 볼 수 있습니다.
이후 똑같이 순서대로 들어오는 데이터를 받습니다.
이제 BehaviorSubject 가 대략적으로 어떤 식인지 알수 있겠죠?
이외에도 다양한 Stream 기법이 RxJS 에 포함되어있으니 한번 살펴보시기 바랍니다.
코드로 돌아가서 마지막으로 해당 서비스를 싱글톤 으로 만들기 위해 별도의 TodoService 변수를 만들어 export 합니다.
이제 Todo 데이터들을 저장할 공간, 조작할 메소드가 완성 되었으니 UI 를 만들어 봅시다.
먼저 해당 UI 에 사용 될 Style 코드 입니다. 간단한 예제 이기 때문에 Style 역시 한 곳에서 간단하게 만들었습니다.
import CSS from 'csstype'; /** * TODO LIST WRAP */ export const kListWrap: CSS.Properties = { display: 'flex', height: '100%', alignItems: 'center', flexDirection: 'column', padding: '2em 4em', }; /** * TODO CONTENT WRAP */ export const kTodoContentWrap: CSS.Properties = { marginBottom: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', flexDirection: 'column', }; /** * TODO CONTENT */ export const kTodoContentTitle: CSS.Properties = { fontSize: '1.6rem' }; /** * TODO ADD BUTTON, TODO DELETE BUTTON */ export const kButton: CSS.Properties = { fontSize: '0.9rem', borderRadius: '6px', padding: '0.4em 0.8em', marginRight: '0.8em' }; export const kTodoInput: CSS.Properties = { fontSize: '0.9rem', padding: '0.4em', width: '400px', marginBottom: '10px', }; /** * TODO TOGGLE */ export const kToggleCheckBox: CSS.Properties = { zoom: '1.5', padding: '10px' }; /** * TODO BUTTON WRAP */ export const kButtonWrap: CSS.Properties = { display: 'flex', alignItems: 'center', }; /** * TODO ADD */ export const kInputFormWrap: CSS.Properties = { display: 'flex', alignItems: 'center', flexDirection: 'column', marginBottom: '20px' };
작은 컴포넌트 부터 만들어 봅시다.
삭제 버튼과 토글 체크박스를 만듭시다.
// 삭제버튼 import React from 'react'; import { kButton } from '../constants/style'; interface TodoDeleteButtonProps { onDelete: (id: number) => void; todoId: number } const TodoDeleteButton: React.FC<TodoDeleteButtonProps> = ({ onDelete, todoId }) => { return ( <button style={kButton} onClick={() => onDelete(todoId)}>삭제</button> ) }; export default React.memo(TodoDeleteButton);
// 토글 버튼 import React from 'react'; import { TodoItem } from '../models'; import { kToggleCheckBox } from '../constants/style'; interface TodoToggleProps { onToggle: (id: number, checked: boolean) => void, todo: TodoItem } const TodoToggle: React.FC<TodoToggleProps> = ({ onToggle, todo }) => { const changeIsDone = (e: any) => onToggle(todo.id, e.target.checked); return ( <input type="checkbox" style={kToggleCheckBox} checked={todo.isDone} onChange={changeIsDone}> </input> ) }; export default React.memo(TodoToggle);
다음으로 Todo 를 표현할 TodoItem 컴포넌트 입니다.
// TODO ITEM import React from 'react'; import { TodoItem } from '../models'; import { kTodoContentTitle } from '../constants/style'; interface TodoProps { todoItem: TodoItem } const Todo: React.FC<TodoProps> = ({ todoItem }) => { return ( <p style={{ ...kTodoContentTitle, textDecoration: todoItem.isDone ? 'line-through' : 'none' }}> {todoItem.content} </p> ) } export default React.memo(Todo);
마지막으로 Todo 를 추가하는 부분입니다.
import React, { useState } from 'react'; import { kButton, kTodoInput, kInputFormWrap } from '../constants/style'; interface TodoAddProps { addTodo: (content: string) => void; } const TodoAdd: React.FC<TodoAddProps> = ({ addTodo }) => { const [content, setContent] = useState(''); const changeContent = (event: any) => setContent(event.target.value); const submit = (event: any) => { event.preventDefault(); addTodo(content); setContent(''); } return ( <> <form onSubmit={submit} style={kInputFormWrap}> <input type="text" style={kTodoInput} value={content} onChange={changeContent} /> <button type="submit" style={kButton}>추가</button> </form> <hr style={{ width: '100%' }} /> </> ) } export default React.memo(TodoAdd);
내부에 들어갈 컴포넌트 들은 마무리 되었습니다.
해당 컴포넌트 들에 대한 자세한 설명이 없는 건 이 글은 React 자체를 설명하는 글이 아닌
RxJS 를 이용한 상태관리에 대하여 설명하는 글 이기 때문입니다.
그리고 React 를 사용하는 분들 이라면 별도의 설명이 필요없는 단순한 컴포넌트 들 이기도 합니다.
이제 각 부분을 Wrap 할 공통 컴포넌트를 만듭니다.
import React from 'react'; import CSS from 'csstype'; interface TodoWrapProps { children: React.ReactNode, style: CSS.Properties } const TodoWrap: React.FC<TodoWrapProps> = props => { return ( <div style={props.style}> {props.children} </div> ) }; export default React.memo(TodoWrap);
각 컴포넌트를 조합하여 TodoList Container 를 만들어 봅시다.
import React from 'react'; import { TodoItem } from '../models'; import Todo from '../components/Todo'; import { TodoService } from '../services'; import { kListWrap, kTodoContentWrap, kButtonWrap } from '../constants/style'; import { TodoWrap, TodoDeleteButton, TodoAdd, TodoToggle } from '../components'; interface TodoListProps { todos: TodoItem[]; } const TodoList: React.FC<TodoListProps> = ({ todos }) => { const deleteTodoItem = (id: number) => TodoService.deleteTodo(id); const addTodoItem = (content: string) => TodoService.addTodo(content); const onToggle = (id: number, checked: boolean) => TodoService.toggleIsDone(id, checked); return ( <TodoWrap style={kListWrap}> <TodoAdd addTodo={addTodoItem} /> {!!todos && todos.map((item: TodoItem) => <TodoWrap style={kTodoContentWrap} key={item.id}> <Todo todoItem={item}></Todo> <TodoWrap style={kButtonWrap}> <TodoDeleteButton onDelete={deleteTodoItem} todoId={item.id} /> <TodoToggle onToggle={onToggle} todo={item} ></TodoToggle> </TodoWrap> </TodoWrap> )} </TodoWrap> ) } export default React.memo(TodoList);
컨테이너를 완성 했으면 데이터를 구독 할 Page 컴포넌트를 마지막으로 만듭니다.
import React, { useEffect, useState } from 'react'; import { TodoService } from '../services/my-service'; import { TodoItem } from '../models'; import TodoList from '../containers/TodoList'; import { Subscription } from 'rxjs'; const TodoPage: React.FC = () => { const [todos, setTodos] = useState(); useEffect(() => { const todoData$: Subscription = TodoService.todoData$ .subscribe((v: TodoItem[]) => { setTodos(v); }); return () => todoData$.unsubscribe(); }, []); return ( <TodoList todos={todos} /> ) } export default TodoPage;
useEffect 를 사용하여 컴포넌트rk 렌더링 될 때 TodoService 의 tododata 를 구독하여 데이터를 가져오고 있습니다.
그리고 해당 컴포넌트가 사라질때 unsubscribe 를 사용하여 구독을 취소 하고 있습니다.
여기서 별도의 Subscription 을 만들어서 사용하는 이유는 구독하고 있는 컴포넌트 를 더이상 사용하지 않을때
구독을 꼭 취소해야 메모리 누수를 막을 수 있습니다.
그럼 미리 만들어 둔 TodoService 의 dispose 를 사용하면 되지 않느냐는 의문이 들 수도 있겠지만
dispose 를 사용하면 Subject 를 완전히 종료하게 됩니다.
싱글톤 으로 만들어진 서비스에서 Subject 를 종료하게 되면 해당 Subject 를 구독 중인 다른 구독자 들도
해당 Subject 를 이용하지 못 하게 됩니다.
현재 프로그램에서는 한 개의 Page 를 쓰고 구독을 하나밖에 하지 않기 때문에 상관 없지만,
여러 구독자가 있을때 없애 버리면 안되겠죠?
그래서 별도의 Subscription 을 만들고 Subject 완료가 아닌 구독만 취소 하는 것 입니다.
이제 완성되었습니다.
Todo 데이터를 구독 하고 있기 때문에 데이터의 조작이 일어나면 자동으로 방출된 데이터를 받아 반응 하게 됩니다.
그리고 원하는 곳에서 TodoService 를 가져다 쓰기만 하면 됩니다.
오랜만에 React 를 사용 할 일이 있어서 React Hooks 도 연습하고 사용 해볼 겸 간단하게 만들었습니다.
최적화 부분이나 잘못된 부분은 조언 부탁드립니다.
풀 소스코드는 아래에 있습니다.
https://github.com/SiWonRyu/ReactWIthRxjs
p.s : 티스토리 기본 코드지원은 업데이트 되도 그리 좋지 않네요...
새로운 블로그에서 새로운 rxjs in react 글을 만나세요.
https://willowryu.github.io/