sbyeol3/articles

[번역] React에서 BLoC 패턴 사용하기

Opened this issue · 0 comments

2021년 7월 28일에 작성된 Using BLoC Pattern with React을 읽고 번역한 글입니다.

처음에 BLoC(Business Logic Component) 패턴은 구글이 플러터 앱에서 상태를 관리하기 위한 방법으로 소개했습니다.
이 패턴은 컴포넌트에서 비즈니스 로직을 분리함으로써 UI 컴포넌트의 워크로드를 줄여줍니다.

시간이 지나며 다른 프레임워크 또한 BLoC 패턴을 지원하기 시작했습니다.
그리고 이 글에서는 리액트에서 BLoC 패턴을 사용하는 방법에 대해 이야기 해보고자 합니다.

리액트에서 BLoC 패턴을 사용하면 얻는 장점

BLoC 패턴 뒤에 있는 개념은 간단합니다. 위에서 볼 수 있듯이 UI 컴포넌트에서 비즈니스 로직을 분리합니다.
먼저 옵저버를 사용하여 BLoC에 이벤트를 보냅니다. 그 후 요청을 처리하면 UI 컴포넌트들은 BLoC의 옵저버블을 통해 알게 됩니다.

자, 이 접근법을 사용할 때의 장점을 자세하게 살펴봅시다.

1. 어플리케이션 로직을 업데이트하는 유연함

비즈니스 로직이 UI 컴포넌트로부터 독립적일 때 어플리케이션에 가해지는 영향도 작아질 것입니다.
UI 컴포넌트에 어떠한 영향을 주지 않고 비즈니스 로직을 변경하는 것도 가능합니다.

2. 로직의 재사용

비즈니스 로직이 한 군데에 있다면, UI 컴포넌트는 중복되는 코드 없이 로직을 재사용하여 앱의 단순함을 증가시킵니다.

3. 테스트가 쉬워짐

테스트케이스를 작성할 때 개발자들은 BLoC 자체에만 집중하면 됩니다. 코드베이스가 지저분해지지 않습니다.

4. 확장성

시간이 지나며 어플리케이션 요구사항은 변경될 수 있고 비즈니스 로직도 커지게 됩니다.
이런 상황에서 코드베이스의 명료함을 유지하고자 개발자들은 여러 BLoC를 생성할 수 있습니다.

게다가 BLoC 패턴은 플랫폼이나 환경에 종속되지 않아 개발자들은 다양한 프로젝트에서 동일한 BLoC 패턴을 사용할 수 있습니다.

개념에서 실전으로

BLoC 패턴의 사용성을 입증하고자 작은 카운터 앱을 만들어봅시다.

1단계 : 리액트 앱을 생성하고 구조를 만듭니다.

먼저 리액트 앱을 생성해야 합니다. 저는 이 앱을 bloc-counter-app이라 이름 짓겠습니다. 또한 rxjs도 사용하겠습니다.

// Create React app
npx create-react-app bloc-counter-app
// Install rxjs
yarn add rxjs

그 후 필요없는 코드를 모두 지우고 아래 목록에 따라 구조를 만듭니다.

  • Blocs - 필요한 bloc 클래스 저장
  • Components - UI 컴포넌트 저장
  • Utils - 프로젝트의 유틸리티 파일 저장

2단계 : BLoC를 구현합니다

이제 BLoC 클래스를 구현해봅시다. BLoC 클래스는 비즈니스 로직과 관련된 모든 subject를 구현하는 역할을 합니다.
이 예시에서는 카운터 로직에 대한 역할을 수행하죠.

bloc 디렉토리 내에 CounterBloc.js라는 파일을 생성하고 pipe를 사용하여 UI 컴포넌트에 카운터를 전달했습니다.

import { Subject } from 'rxjs';
import { scan } from 'rxjs/operators';

export default class CounterBloc {
  constructor() {
    this._subject = new Subject();
  }

  get counter() {
    return this._subject.pipe(scan((count, v) => count + v, 0));
  }

  increase() {
    this._subject.next(1);
  }

  decrease() {
    this._subject.next(-1);
  }

  dispose() {
    this._subject.complete();
  }
}

클래스에는 간단한 로직만 있습니다. 그러나 앱의 사이즈가 커졌는데 비즈니스 로직을 분리하지 않았을 때 얼마나 복잡할지 상상해보세요.

3단계 : 중간 클래스로 BLoC에 아름다움을 더하세요.

이 단계에서 UI로부터 오는 카운터 요청을 다루기 위해 utils 디렉토리 내에 StreamBuilder.js 파일을 생성합니다.
게다가 개발자들은 에러를 처리하고 customer 핸들러를 구현해야 합니다.

class AsyncSnapshot {
  constructor(data, error) {
    this._data = data;
    this._error = error;
    this._hasData = data ? true : false;
    this._hasError = error ? true : false;
  }

  get data() {
    return this._data;
  }

  get error() {
    return this._error;
  }

  get hasData() {
    return this._hasData;
  }

  get hasError() {
    return this._hasError;
  }
}

AsyncSnapshot 클래스 내에서 생성자를 초기화하고 데이터를 다루고, 에러를 처리할 수 있습니다.
그러나 이 예제에서는 설명을 쉽게 하기 위해 단순히 데이터만 리턴합니다.

class StreamBuilder extends Component {
  constructor(props) {
    super(props);

    const { initialData, stream } = props;

    this.state = {
      snapshot: new AsyncSnapshot(initialData),
    };

    stream.subscribe(
      data => {
        this.setState({
          snapshot: new AsyncSnapshot(data, null),
        });
      }
    );
  }

  render() {
    return this.props.builder(this.state.snapshot);
  }
}

초기 데이터는 AysncSnapshot에 전달되어 각 구독마다 스냅샷 상태 내에 저장됩니다.
그 후 UI 컴포넌트 내에서 렌더될 것입니다.

import { Component } from 'react';

class AsyncSnapshot {
  constructor(data) {
    this._data = data;
  }
  get data() {
    return this._data;
  }
}

class StreamBuilder extends Component {
  constructor(props) {
    super(props);

    const { initialData, stream } = props;

    this.state = {
      snapshot: new AsyncSnapshot(initialData),
    };

    stream.subscribe(
      data => {
        this.setState({
          snapshot: new AsyncSnapshot(data, null),
        });
      }
    );
  }

  render() {
    return this.props.builder(this.state.snapshot);
  }
}

export default StreamBuilder;

주의 : UI 컴포넌트가 언마운트 될 때 모든 옵저버블로부터 구독을 해제하고 BLoC를 폐기해야 합니다.

4단계 : UI 컴포넌트를 구현합니다

increase()decrease() 메소드들은 UI 컴포넌트 내에서 바로 호출됩니다. 그러나 출력 데이터는 stream builder에 의해 다뤄집니다.

에러를 처리하기 위해 커스텀 핸들러를 구현하는 중간 층을 두는 것이 좋습니다.

import { Fragment } from 'react';

import StreamBuilder from '../utils/StreamBuilder';

const Counter = ({ bloc }) => (
    <Fragment>
        <button onClick={() => bloc.increase()}>+</button>
        <button onClick={() => bloc.decrease()}>-</button>
        <lable size="large" color="olive">
            Count:
            <StreamBuilder
                initialData={0}
                stream={bloc.counter}
                builder={snapshot => <p>{snapshot.data}</p>}
            />
        </lable>
    </Fragment>
);

export default Counter;

app.js 파일에서 BLoC는 CounterBloc 클래스를 사용하여 초기화됩니다.
그래서 Counter 컴포넌트는 BLoC를 prop으로 전달하여 사용됩니다.

// App.js
import React, { Component } from 'react';
import Counter from './components/Counter';
import CounterBloc from './blocs/CounterBloc';

const bloc = new CounterBloc();

class App extends Component {
  componentWillUnmount() {
    bloc.dispose();
  }
  render() {
    return (
      <div>
        <Counter bloc={bloc} />
      </div>
    );
  }
}
export default App;

이것이 전부입니다. 이제 여러분은 UI 컴포넌트 밖에서 비즈니스 로직을 분리된 엔터티로서 처리할 수 있고 변경할 수 있습니다.
이 예제 앱을 사용하고 개선시키고 싶다면 프로젝트 저장소를 참조하시고 PR을 날려주세요. 😃

정리

제 경험에 비추어 보면, BLoC 패턴은 작은 규모의 프로젝트에서는 오히려 오버헤드가 될 수 있습니다.
그러나 프로젝트가 커질수록 BLoC 패턴은 모듈화된 앱을 만드는 데 도움이 됩니다.

또한 rxjs에 대한 기본적인 이해와 어떻게 옵저버블이 BLoC 패턴을 구현하는지를 아는 것이 필요합니다.

저는 여러분에게 BLoC 패턴을 사용하도록 초대했으니 댓글에서 여러분의 생각을 공유해주세요.
읽어주셔서 감사합니다 !!!

Bit을 사용하여 독립적인 JS 컴포넌트를 생성하고 공유하세요

**Bit**은 독립적으로 작성되고 버전 관리되며 유지되는 컴포넌트를 사용하여 완전히 모듈화된 앱을 생성하도록 하는 확장 가능한 툴입니다.
모듈러 앱과 디자인 시스템을 설계하거나 마이크로 프론트엔드를 작성하고 제공하거나 앱 사이에서 컴포넌트를 공유할 때 사용해보세요.