ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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/

    댓글